@rkosafo/cai.components 0.0.78 → 0.0.80
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/README.md +8 -8
- package/dist/baseEditor/index.svelte +32 -32
- package/dist/builders/filters/FilterBuilder.svelte +641 -641
- package/dist/forms/FormCheckbox/FormCheckbox.svelte +53 -53
- package/dist/forms/FormClEditor/ClEdito.svelte +68 -68
- package/dist/forms/FormDatepicker/FormDatepicker.svelte +159 -159
- package/dist/forms/FormFileUpload/FormFileUplad.svelte +134 -134
- package/dist/forms/FormInput/FormInput.svelte +87 -87
- package/dist/forms/FormRadio/FormRadio.svelte +53 -53
- package/dist/forms/FormSelect/FormSelect.svelte +88 -88
- package/dist/forms/FormTextarea/FormTextarea.svelte +78 -78
- package/dist/forms/button-toggle/ButtonToggle.svelte +119 -119
- package/dist/forms/button-toggle/CheckIcon.svelte +28 -28
- package/dist/forms/checkbox/Checkbox.svelte +82 -82
- package/dist/forms/checkbox/CheckboxButton.svelte +92 -92
- package/dist/forms/datepicker/Datepicker.svelte +707 -707
- package/dist/forms/form/Form.svelte +69 -69
- package/dist/forms/input/Input.svelte +363 -363
- package/dist/forms/label/Label.svelte +38 -38
- package/dist/forms/radio/Radio.svelte +48 -48
- package/dist/forms/radio/RadioButton.svelte +22 -22
- package/dist/forms/select/Select.svelte +56 -56
- package/dist/forms/textarea/Textarea.svelte +165 -165
- package/dist/forms/toggle/Toggle.svelte +70 -70
- package/dist/layout/Chat/CategorySelector.svelte +52 -52
- package/dist/layout/Chat/ChatEntry.svelte +246 -246
- package/dist/layout/Chat/ChatEntrySkeleton.svelte +81 -81
- package/dist/layout/Chat/ChatHeader.svelte +172 -172
- package/dist/layout/Chat/ChatInput.svelte +207 -207
- package/dist/layout/Chat/DraggableWindow.svelte +230 -230
- package/dist/layout/Chat/PreviewPage.svelte +182 -182
- package/dist/layout/Chat/RichText.svelte +216 -216
- package/dist/layout/ComponentCanvas/Canvas.svelte +40 -40
- package/dist/layout/ComponentCanvas/ComponentRenderer.svelte +85 -85
- package/dist/layout/TF/Content/Content.svelte +21 -21
- package/dist/layout/TF/Header/Header.svelte +166 -166
- package/dist/layout/TF/Sidebar/Sidebar.svelte +148 -148
- package/dist/layout/TF/Wrapper/Wrapper.svelte +17 -17
- package/dist/layout/mailing/MailPaginator.svelte +36 -36
- package/dist/layout/mailing/MailSidebar.svelte +39 -39
- package/dist/layout/mailing/MailToolBar.svelte +174 -174
- package/dist/layout/mailing/MailingContent.svelte +10 -10
- package/dist/layout/mailing/MailingHeader.svelte +55 -55
- package/dist/layout/mailing/MailingMessageCard.svelte +112 -112
- package/dist/layout/mailing/MailingMessageViewer.svelte +87 -87
- package/dist/layout/mailing/MailingModule.svelte +448 -448
- package/dist/styles/docs.css +615 -615
- package/dist/styles/tf-layout.css +185 -185
- package/dist/themes/ThemeProvider.svelte +20 -20
- package/dist/types/index.d.ts +2 -0
- package/dist/typography/heading/Heading.svelte +35 -35
- package/dist/ui/accordion/Accordion.svelte +49 -49
- package/dist/ui/accordion/AccordionItem.svelte +173 -173
- package/dist/ui/alert/Alert.svelte +83 -83
- package/dist/ui/alertDialog/AlertDialog.svelte +40 -40
- package/dist/ui/avatar/Avatar.svelte +77 -77
- package/dist/ui/box/Box.svelte +28 -28
- package/dist/ui/breadcrumb/Breadcrumb.svelte +39 -39
- package/dist/ui/buttons/ActionButton.svelte +234 -234
- package/dist/ui/buttons/Button.svelte +102 -102
- package/dist/ui/buttons/GradientButton.svelte +59 -59
- package/dist/ui/datatable/Datatable.svelte +525 -525
- package/dist/ui/drawer/Drawer.svelte +300 -300
- package/dist/ui/dropdown/Dropdown.svelte +36 -36
- package/dist/ui/dropdown/DropdownDivider.svelte +11 -11
- package/dist/ui/dropdown/DropdownGroup.svelte +14 -14
- package/dist/ui/dropdown/DropdownHeader.svelte +14 -14
- package/dist/ui/dropdown/DropdownItem.svelte +52 -52
- package/dist/ui/footer/Footer.svelte +15 -15
- package/dist/ui/footer/FooterBrand.svelte +37 -37
- package/dist/ui/footer/FooterCopyright.svelte +45 -45
- package/dist/ui/footer/FooterIcon.svelte +22 -22
- package/dist/ui/footer/FooterLink.svelte +33 -33
- package/dist/ui/footer/FooterLinkGroup.svelte +13 -13
- package/dist/ui/icons/IconifyIcon.svelte +7 -7
- package/dist/ui/indicator/Indicator.svelte +42 -42
- package/dist/ui/modal/Modal.svelte +265 -265
- package/dist/ui/notificationList/NotificationList.svelte +123 -123
- package/dist/ui/pageLoader/PageLoader.svelte +14 -14
- package/dist/ui/pageLoader/PageLoader2.svelte +99 -0
- package/dist/ui/pageLoader/PageLoader2.svelte.d.ts +24 -0
- package/dist/ui/pageLoader/index.d.ts +2 -1
- package/dist/ui/pageLoader/index.js +2 -1
- package/dist/ui/paginate/Paginate.svelte +96 -96
- package/dist/ui/speedDial/SpeedDial.svelte +77 -77
- package/dist/ui/speedDial/SpeedDialButton.svelte +75 -75
- package/dist/ui/speedDial/SpeedDialTrigger.svelte +79 -79
- package/dist/ui/tab/Tab.svelte +93 -67
- package/dist/ui/table/Table.svelte +396 -396
- package/dist/ui/tableLoader/TableLoader.svelte +24 -24
- package/dist/ui/toast/Toast.svelte +337 -337
- package/dist/ui/toast/Toast.svelte.d.ts +10 -10
- package/dist/ui/toolbar/Toolbar.svelte +59 -59
- package/dist/ui/toolbar/ToolbarButton.svelte +56 -56
- package/dist/ui/toolbar/ToolbarGroup.svelte +43 -43
- package/dist/ui/tooltip/Tooltip.svelte +51 -51
- package/dist/utils/Popper.svelte +257 -257
- package/dist/utils/closeButton/CloseButton.svelte +88 -88
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +3 -3
- package/dist/utils/singleSelection.svelte.js +48 -48
- package/dist/youtube/index.svelte +12 -12
- package/package.json +1 -1
|
@@ -1,707 +1,707 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { onMount } from 'svelte';
|
|
3
|
-
import { fade } from 'svelte/transition';
|
|
4
|
-
import clsx from 'clsx';
|
|
5
|
-
import { datepicker } from './index.js';
|
|
6
|
-
import {
|
|
7
|
-
parse,
|
|
8
|
-
isValid,
|
|
9
|
-
addDays,
|
|
10
|
-
startOfMonth,
|
|
11
|
-
endOfMonth,
|
|
12
|
-
startOfWeek,
|
|
13
|
-
endOfWeek,
|
|
14
|
-
eachDayOfInterval,
|
|
15
|
-
isSameDay,
|
|
16
|
-
isWithinInterval
|
|
17
|
-
} from 'date-fns';
|
|
18
|
-
import { Button, getTheme, ToolbarButton, type DatepickerProps } from '../../index.js';
|
|
19
|
-
|
|
20
|
-
let {
|
|
21
|
-
value = $bindable(),
|
|
22
|
-
defaultDate = null,
|
|
23
|
-
range = false,
|
|
24
|
-
rangeFrom = $bindable(),
|
|
25
|
-
rangeTo = $bindable(),
|
|
26
|
-
availableFrom = null,
|
|
27
|
-
availableTo = null,
|
|
28
|
-
locale = 'default',
|
|
29
|
-
translationLocale = locale,
|
|
30
|
-
firstDayOfWeek = 0,
|
|
31
|
-
dateFormat,
|
|
32
|
-
placeholder = 'Select date',
|
|
33
|
-
disabled = false,
|
|
34
|
-
required = false,
|
|
35
|
-
inputClass = '',
|
|
36
|
-
color = 'primary',
|
|
37
|
-
inline = false,
|
|
38
|
-
autohide = true,
|
|
39
|
-
showActionButtons = false,
|
|
40
|
-
title = '',
|
|
41
|
-
onselect,
|
|
42
|
-
onclear,
|
|
43
|
-
onapply,
|
|
44
|
-
btnClass,
|
|
45
|
-
inputmode = 'none',
|
|
46
|
-
classes,
|
|
47
|
-
monthColor = 'alternative',
|
|
48
|
-
monthBtnSelected = 'bg-primary-500 text-white',
|
|
49
|
-
monthBtn = 'text-gray-700 dark:text-gray-300',
|
|
50
|
-
class: className,
|
|
51
|
-
elementRef = $bindable(),
|
|
52
|
-
name,
|
|
53
|
-
// New placement prop
|
|
54
|
-
placement = 'bottom-start'
|
|
55
|
-
}: DatepickerProps = $props();
|
|
56
|
-
|
|
57
|
-
const theme = getTheme('datepicker');
|
|
58
|
-
|
|
59
|
-
// If translationLocale is not explicitly provided, it will default to the value of locale. This ensures reactivity as both are directly exposed as props.
|
|
60
|
-
translationLocale = translationLocale ?? locale;
|
|
61
|
-
|
|
62
|
-
let isOpen: boolean = $state(inline);
|
|
63
|
-
let showMonthSelector: boolean = $state(false);
|
|
64
|
-
let inputElement: HTMLInputElement | null = $state(null);
|
|
65
|
-
|
|
66
|
-
$effect(() => {
|
|
67
|
-
if (inputElement) {
|
|
68
|
-
elementRef = inputElement;
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
let datepickerContainerElement: HTMLDivElement;
|
|
72
|
-
let currentMonth: Date = $state(value || defaultDate || new Date());
|
|
73
|
-
let focusedDate: Date | null = null;
|
|
74
|
-
let calendarRef: HTMLDivElement | null = $state(null);
|
|
75
|
-
|
|
76
|
-
let daysInMonth = $derived(getDaysInMonth(currentMonth));
|
|
77
|
-
|
|
78
|
-
onMount(() => {
|
|
79
|
-
if (!inline) {
|
|
80
|
-
datepickerContainerElement?.ownerDocument.addEventListener('click', handleClickOutside);
|
|
81
|
-
return () => {
|
|
82
|
-
datepickerContainerElement?.ownerDocument.removeEventListener('click', handleClickOutside);
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
function getDaysInMonth(date: Date): Date[] {
|
|
88
|
-
const monthStart = startOfMonth(date);
|
|
89
|
-
const monthEnd = endOfMonth(date);
|
|
90
|
-
const calendarStart = startOfWeek(monthStart, { weekStartsOn: firstDayOfWeek });
|
|
91
|
-
const calendarEnd = endOfWeek(monthEnd, { weekStartsOn: firstDayOfWeek });
|
|
92
|
-
|
|
93
|
-
return eachDayOfInterval({ start: calendarStart, end: calendarEnd });
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const getWeekdayNames = (): string[] => {
|
|
97
|
-
const referenceDate = new Date(1970, 0, 4 + firstDayOfWeek);
|
|
98
|
-
return Array.from({ length: 7 }, (_, i) =>
|
|
99
|
-
addDays(referenceDate, i).toLocaleDateString(translationLocale, { weekday: 'short' })
|
|
100
|
-
);
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
let weekdays = $derived(getWeekdayNames());
|
|
104
|
-
|
|
105
|
-
const getMonthNames = (): string[] => {
|
|
106
|
-
return Array.from({ length: 12 }, (_, i) =>
|
|
107
|
-
new Date(2000, i, 1).toLocaleDateString(translationLocale, { month: 'short' })
|
|
108
|
-
);
|
|
109
|
-
};
|
|
110
|
-
let monthNames = $derived(getMonthNames());
|
|
111
|
-
|
|
112
|
-
const addDay = (date: Date, increment: number): Date => addDays(date, increment);
|
|
113
|
-
|
|
114
|
-
function changeMonth(increment: number) {
|
|
115
|
-
currentMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + increment, 1);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function changeYear(increment: number) {
|
|
119
|
-
currentMonth = new Date(currentMonth.getFullYear() + increment, currentMonth.getMonth(), 1);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function selectMonth(monthIndex: number, event: MouseEvent) {
|
|
123
|
-
event.stopPropagation();
|
|
124
|
-
currentMonth = new Date(currentMonth.getFullYear(), monthIndex, 1);
|
|
125
|
-
showMonthSelector = false;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function toggleMonthSelector(event: MouseEvent) {
|
|
129
|
-
event.stopPropagation();
|
|
130
|
-
showMonthSelector = !showMonthSelector;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function isDateAvailable(date: Date): boolean {
|
|
134
|
-
const dateOnly = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
135
|
-
|
|
136
|
-
if (availableFrom) {
|
|
137
|
-
const fromDate = new Date(
|
|
138
|
-
availableFrom.getFullYear(),
|
|
139
|
-
availableFrom.getMonth(),
|
|
140
|
-
availableFrom.getDate()
|
|
141
|
-
);
|
|
142
|
-
if (dateOnly < fromDate) return false;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (availableTo) {
|
|
146
|
-
const toDate = new Date(
|
|
147
|
-
availableTo.getFullYear(),
|
|
148
|
-
availableTo.getMonth(),
|
|
149
|
-
availableTo.getDate()
|
|
150
|
-
);
|
|
151
|
-
if (dateOnly > toDate) return false;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return true;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function handleDaySelect(day: Date) {
|
|
158
|
-
if (!isDateAvailable(day)) return;
|
|
159
|
-
|
|
160
|
-
if (range) {
|
|
161
|
-
if (!rangeFrom || (rangeFrom && rangeTo)) {
|
|
162
|
-
rangeFrom = day;
|
|
163
|
-
rangeTo = undefined;
|
|
164
|
-
} else if (day < rangeFrom) {
|
|
165
|
-
rangeFrom = day;
|
|
166
|
-
rangeTo = rangeFrom;
|
|
167
|
-
} else {
|
|
168
|
-
rangeTo = day;
|
|
169
|
-
}
|
|
170
|
-
onselect?.({ from: rangeFrom, to: rangeTo });
|
|
171
|
-
} else {
|
|
172
|
-
value = day;
|
|
173
|
-
onselect?.(value);
|
|
174
|
-
if (autohide && !inline) isOpen = false;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function handleInputChangeWithDateFns() {
|
|
179
|
-
const inputValue = inputElement?.value?.trim();
|
|
180
|
-
if (!inputValue) {
|
|
181
|
-
rangeFrom = undefined;
|
|
182
|
-
rangeTo = undefined;
|
|
183
|
-
inputElement?.setCustomValidity('');
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
inputElement?.setCustomValidity('');
|
|
188
|
-
|
|
189
|
-
if (range) {
|
|
190
|
-
const parts = inputValue.split(' - ');
|
|
191
|
-
if (parts.length === 2) {
|
|
192
|
-
const parsedFrom = tryParseDate(parts[0]);
|
|
193
|
-
const parsedTo = tryParseDate(parts[1]);
|
|
194
|
-
|
|
195
|
-
if (
|
|
196
|
-
parsedFrom &&
|
|
197
|
-
isValid(parsedFrom) &&
|
|
198
|
-
isDateAvailable(parsedFrom) &&
|
|
199
|
-
parsedTo &&
|
|
200
|
-
isValid(parsedTo) &&
|
|
201
|
-
isDateAvailable(parsedTo)
|
|
202
|
-
) {
|
|
203
|
-
[rangeFrom, rangeTo] =
|
|
204
|
-
parsedFrom > parsedTo ? [parsedTo, parsedFrom] : [parsedFrom, parsedTo];
|
|
205
|
-
onselect?.({ from: rangeFrom, to: rangeTo });
|
|
206
|
-
return;
|
|
207
|
-
} else {
|
|
208
|
-
inputElement?.setCustomValidity(
|
|
209
|
-
`Please enter date range in format: ${getDateFormatPattern()} - ${getDateFormatPattern()}`
|
|
210
|
-
);
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const parsedDate = tryParseDate(inputValue);
|
|
217
|
-
|
|
218
|
-
if (!parsedDate || !isValid(parsedDate)) {
|
|
219
|
-
const formatPattern = getDateFormatPattern();
|
|
220
|
-
inputElement?.setCustomValidity(`Please enter date in format: ${formatPattern}`);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (!isDateAvailable(parsedDate)) {
|
|
225
|
-
inputElement?.setCustomValidity('Selected date is not available');
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
handleDaySelect(parsedDate);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function tryParseDate(inputValue: string): Date | null {
|
|
233
|
-
const formatPattern = getDateFormatPattern();
|
|
234
|
-
try {
|
|
235
|
-
const parsedDate = parse(inputValue, formatPattern, new Date());
|
|
236
|
-
if (isValid(parsedDate)) {
|
|
237
|
-
return parsedDate;
|
|
238
|
-
}
|
|
239
|
-
} catch (error) {
|
|
240
|
-
// Continue to next strategy
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const commonFormats = [
|
|
244
|
-
'd.M.yyyy', // German: 17.7.2025
|
|
245
|
-
'dd.MM.yyyy', // German: 17.07.2025
|
|
246
|
-
'M/d/yyyy', // US: 7/17/2025
|
|
247
|
-
'MM/dd/yyyy', // US: 07/17/2025
|
|
248
|
-
'd/M/yyyy', // UK: 17/7/2025
|
|
249
|
-
'dd/MM/yyyy', // UK: 17/07/2025
|
|
250
|
-
'yyyy-MM-dd', // ISO: 2025-07-17
|
|
251
|
-
'yyyy-M-d', // ISO: 2025-7-17
|
|
252
|
-
'M-d-yyyy', // US with dashes: 7-17-2025
|
|
253
|
-
'd-M-yyyy' // EU with dashes: 17-7-2025
|
|
254
|
-
];
|
|
255
|
-
|
|
256
|
-
for (const format of commonFormats) {
|
|
257
|
-
try {
|
|
258
|
-
const parsedDate = parse(inputValue, format, new Date());
|
|
259
|
-
if (isValid(parsedDate)) {
|
|
260
|
-
return parsedDate;
|
|
261
|
-
}
|
|
262
|
-
} catch (error) {
|
|
263
|
-
// Continue to next format
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
try {
|
|
268
|
-
const nativeDate = new Date(inputValue);
|
|
269
|
-
if (isValid(nativeDate) && !isNaN(nativeDate.getTime())) {
|
|
270
|
-
return nativeDate;
|
|
271
|
-
}
|
|
272
|
-
} catch (error) {
|
|
273
|
-
// Continue
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return null;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function getDateFormatPattern(): string {
|
|
280
|
-
const actualLocale = locale === 'default' ? navigator.language : locale;
|
|
281
|
-
const testDate = new Date(2025, 0, 15); // January 15, 2025
|
|
282
|
-
const formatted = testDate.toLocaleDateString(
|
|
283
|
-
actualLocale,
|
|
284
|
-
dateFormat || { year: 'numeric', month: 'numeric', day: 'numeric' }
|
|
285
|
-
);
|
|
286
|
-
|
|
287
|
-
if (formatted.includes('.')) {
|
|
288
|
-
// German/European format with dots
|
|
289
|
-
if (formatted.startsWith('15.')) {
|
|
290
|
-
return 'd.M.yyyy';
|
|
291
|
-
} else if (formatted.startsWith('01.')) {
|
|
292
|
-
return 'M.d.yyyy';
|
|
293
|
-
}
|
|
294
|
-
return 'd.M.yyyy'; // Default to day first
|
|
295
|
-
} else if (formatted.includes('/')) {
|
|
296
|
-
// US/UK format with slashes
|
|
297
|
-
if (formatted.startsWith('1/')) {
|
|
298
|
-
return 'M/d/yyyy'; // US format
|
|
299
|
-
} else if (formatted.startsWith('15/')) {
|
|
300
|
-
return 'd/M/yyyy'; // UK format
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const testDate2 = new Date(2025, 11, 3); // December 3, 2025
|
|
304
|
-
const formatted2 = testDate2.toLocaleDateString(
|
|
305
|
-
actualLocale,
|
|
306
|
-
dateFormat || { year: 'numeric', month: 'numeric', day: 'numeric' }
|
|
307
|
-
);
|
|
308
|
-
if (formatted2.startsWith('3/') || formatted2.startsWith('03/')) {
|
|
309
|
-
return 'd/M/yyyy';
|
|
310
|
-
} else {
|
|
311
|
-
return 'M/d/yyyy';
|
|
312
|
-
}
|
|
313
|
-
} else if (formatted.includes('-')) {
|
|
314
|
-
// ISO or other dash format
|
|
315
|
-
if (formatted.startsWith('2025-')) {
|
|
316
|
-
return 'yyyy-M-d';
|
|
317
|
-
} else if (formatted.startsWith('1-')) {
|
|
318
|
-
return 'M-d-yyyy';
|
|
319
|
-
} else {
|
|
320
|
-
return 'd-M-yyyy';
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Default fallback - try to detect based on locale
|
|
325
|
-
if (actualLocale.startsWith('en-US')) {
|
|
326
|
-
return 'M/d/yyyy';
|
|
327
|
-
} else if (
|
|
328
|
-
actualLocale.startsWith('de') ||
|
|
329
|
-
actualLocale.startsWith('at') ||
|
|
330
|
-
actualLocale.startsWith('ch')
|
|
331
|
-
) {
|
|
332
|
-
return 'd.M.yyyy';
|
|
333
|
-
} else if (actualLocale.startsWith('en-GB') || actualLocale.startsWith('en-AU')) {
|
|
334
|
-
return 'd/M/yyyy';
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return 'M/d/yyyy';
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function handleClickOutside(event: MouseEvent) {
|
|
341
|
-
if (
|
|
342
|
-
isOpen &&
|
|
343
|
-
datepickerContainerElement &&
|
|
344
|
-
!datepickerContainerElement.contains(event.target as Node)
|
|
345
|
-
) {
|
|
346
|
-
isOpen = false;
|
|
347
|
-
showMonthSelector = false;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Use locale for formatting (not translationLocale)
|
|
352
|
-
const formatDate = (date?: Date): string => date?.toLocaleDateString(locale, dateFormat) ?? '';
|
|
353
|
-
const isSameDate = (date1?: Date, date2?: Date): boolean =>
|
|
354
|
-
date1 && date2 ? isSameDay(date1, date2) : false;
|
|
355
|
-
const isToday = (day: Date): boolean => isSameDate(day, new Date());
|
|
356
|
-
const isInRange = (day: Date): boolean =>
|
|
357
|
-
!!(range && rangeFrom && rangeTo && isWithinInterval(day, { start: rangeFrom, end: rangeTo }));
|
|
358
|
-
|
|
359
|
-
let isSelected = $derived((day: Date): boolean =>
|
|
360
|
-
range ? isSameDate(day, rangeFrom) || isSameDate(day, rangeTo) : isSameDate(day, value)
|
|
361
|
-
);
|
|
362
|
-
|
|
363
|
-
function handleCalendarKeydown(event: KeyboardEvent) {
|
|
364
|
-
if (!isOpen) return;
|
|
365
|
-
|
|
366
|
-
if (!focusedDate) {
|
|
367
|
-
focusedDate = value || new Date();
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
switch (event.key) {
|
|
371
|
-
case 'ArrowLeft':
|
|
372
|
-
focusedDate = addDay(focusedDate, -1);
|
|
373
|
-
break;
|
|
374
|
-
case 'ArrowRight':
|
|
375
|
-
focusedDate = addDay(focusedDate, 1);
|
|
376
|
-
break;
|
|
377
|
-
case 'ArrowUp':
|
|
378
|
-
focusedDate = addDay(focusedDate, -7);
|
|
379
|
-
break;
|
|
380
|
-
case 'ArrowDown':
|
|
381
|
-
focusedDate = addDay(focusedDate, 7);
|
|
382
|
-
break;
|
|
383
|
-
case 'Enter':
|
|
384
|
-
if (range) {
|
|
385
|
-
if (rangeFrom && rangeTo) {
|
|
386
|
-
if (autohide && !inline) isOpen = false;
|
|
387
|
-
} else {
|
|
388
|
-
handleDaySelect(focusedDate);
|
|
389
|
-
}
|
|
390
|
-
} else {
|
|
391
|
-
handleDaySelect(focusedDate);
|
|
392
|
-
if (autohide && !inline) isOpen = false;
|
|
393
|
-
}
|
|
394
|
-
break;
|
|
395
|
-
case 'Escape':
|
|
396
|
-
isOpen = false;
|
|
397
|
-
showMonthSelector = false;
|
|
398
|
-
inputElement?.focus();
|
|
399
|
-
break;
|
|
400
|
-
default:
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
event.preventDefault();
|
|
405
|
-
if (focusedDate.getMonth() !== currentMonth.getMonth()) {
|
|
406
|
-
currentMonth = new Date(focusedDate.getFullYear(), focusedDate.getMonth(), 1);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Use translationLocale for aria-label
|
|
410
|
-
setTimeout(() => {
|
|
411
|
-
const focusedButton = calendarRef?.querySelector(
|
|
412
|
-
`button[aria-label="${focusedDate!.toLocaleDateString(translationLocale, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}"]`
|
|
413
|
-
) as HTMLButtonElement | null;
|
|
414
|
-
focusedButton?.focus();
|
|
415
|
-
}, 0);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function handleInputKeydown(event: KeyboardEvent) {
|
|
419
|
-
if (event.key === 'Enter') {
|
|
420
|
-
event.preventDefault();
|
|
421
|
-
handleInputChangeWithDateFns();
|
|
422
|
-
if (autohide && !inline) {
|
|
423
|
-
isOpen = false;
|
|
424
|
-
}
|
|
425
|
-
} else if (event.key === ' ') {
|
|
426
|
-
event.preventDefault();
|
|
427
|
-
isOpen = !isOpen;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function handleClear() {
|
|
432
|
-
value = rangeFrom = rangeTo = undefined;
|
|
433
|
-
onclear?.();
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function handleApply() {
|
|
437
|
-
const result = range ? { from: rangeFrom, to: rangeTo } : value;
|
|
438
|
-
if (result) onapply?.(result);
|
|
439
|
-
if (!inline) isOpen = false;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
let {
|
|
443
|
-
base,
|
|
444
|
-
input,
|
|
445
|
-
button,
|
|
446
|
-
titleVariant,
|
|
447
|
-
actionButtons,
|
|
448
|
-
columnHeader,
|
|
449
|
-
polite,
|
|
450
|
-
grid,
|
|
451
|
-
nav,
|
|
452
|
-
dayButton,
|
|
453
|
-
monthButton
|
|
454
|
-
} = datepicker({ placement });
|
|
455
|
-
</script>
|
|
456
|
-
|
|
457
|
-
{#snippet navButton(forward: boolean)}
|
|
458
|
-
<ToolbarButton
|
|
459
|
-
color="dark"
|
|
460
|
-
onclick={() => changeMonth(forward ? 1 : -1)}
|
|
461
|
-
size="lg"
|
|
462
|
-
aria-label={forward ? 'Next month' : 'Previous month'}
|
|
463
|
-
>
|
|
464
|
-
<svg
|
|
465
|
-
class="h-3 w-3 rtl:rotate-180"
|
|
466
|
-
aria-hidden="true"
|
|
467
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
468
|
-
fill="none"
|
|
469
|
-
viewBox="0 0 14 10"
|
|
470
|
-
>
|
|
471
|
-
<path
|
|
472
|
-
stroke="currentColor"
|
|
473
|
-
stroke-linecap="round"
|
|
474
|
-
stroke-linejoin="round"
|
|
475
|
-
stroke-width="2"
|
|
476
|
-
d={forward ? 'M1 5h12m0 0L9 1m4 4L9 9' : 'M13 5H1m0 0 4 4M1 5l4-4'}
|
|
477
|
-
></path>
|
|
478
|
-
</svg>
|
|
479
|
-
</ToolbarButton>
|
|
480
|
-
{/snippet}
|
|
481
|
-
|
|
482
|
-
{#snippet yearNavButton(forward: boolean)}
|
|
483
|
-
<ToolbarButton
|
|
484
|
-
color="dark"
|
|
485
|
-
onclick={() => changeYear(forward ? 1 : -1)}
|
|
486
|
-
size="lg"
|
|
487
|
-
aria-label={forward ? 'Next year' : 'Previous year'}
|
|
488
|
-
>
|
|
489
|
-
<svg
|
|
490
|
-
class="h-3 w-3 rtl:rotate-180"
|
|
491
|
-
aria-hidden="true"
|
|
492
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
493
|
-
fill="none"
|
|
494
|
-
viewBox="0 0 14 10"
|
|
495
|
-
>
|
|
496
|
-
<path
|
|
497
|
-
stroke="currentColor"
|
|
498
|
-
stroke-linecap="round"
|
|
499
|
-
stroke-linejoin="round"
|
|
500
|
-
stroke-width="2"
|
|
501
|
-
d={forward ? 'M1 5h12m0 0L9 1m4 4L9 9' : 'M13 5H1m0 0 4 4M1 5l4-4'}
|
|
502
|
-
></path>
|
|
503
|
-
</svg>
|
|
504
|
-
</ToolbarButton>
|
|
505
|
-
{/snippet}
|
|
506
|
-
|
|
507
|
-
<div bind:this={datepickerContainerElement} class={['relative', inline && 'inline-block']}>
|
|
508
|
-
{#if !inline}
|
|
509
|
-
<div class="relative">
|
|
510
|
-
<input
|
|
511
|
-
bind:this={inputElement}
|
|
512
|
-
type="text"
|
|
513
|
-
class={input({ color, class: clsx(theme?.input, inputClass) })}
|
|
514
|
-
{placeholder}
|
|
515
|
-
value={range && rangeFrom
|
|
516
|
-
? `${formatDate(rangeFrom)} - ${formatDate(rangeTo)}`
|
|
517
|
-
: formatDate(value)}
|
|
518
|
-
onfocus={() => (isOpen = true)}
|
|
519
|
-
onchange={handleInputChangeWithDateFns}
|
|
520
|
-
onkeydown={handleInputKeydown}
|
|
521
|
-
{disabled}
|
|
522
|
-
{required}
|
|
523
|
-
{inputmode}
|
|
524
|
-
aria-haspopup="dialog"
|
|
525
|
-
{name}
|
|
526
|
-
/>
|
|
527
|
-
<button
|
|
528
|
-
type="button"
|
|
529
|
-
class={button({ class: clsx(btnClass, theme?.button, classes?.button) })}
|
|
530
|
-
onclick={() => (isOpen = !isOpen)}
|
|
531
|
-
{disabled}
|
|
532
|
-
aria-label={isOpen ? 'Close date picker' : 'Open date picker'}
|
|
533
|
-
>
|
|
534
|
-
<svg
|
|
535
|
-
class="h-4 w-4 text-gray-500 dark:text-gray-400"
|
|
536
|
-
aria-hidden="true"
|
|
537
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
538
|
-
fill="currentColor"
|
|
539
|
-
viewBox="0 0 20 20"
|
|
540
|
-
>
|
|
541
|
-
<path
|
|
542
|
-
d="M20 4a2 2 0 0 0-2-2h-2V1a1 1 0 0 0-2 0v1h-3V1a1 1 0 0 0-2 0v1H6V1a1 1 0 0 0-2 0v1H2a2 2 0 0 0-2 2v2h20V4ZM0 18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8H0v10Zm5-8h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2Z"
|
|
543
|
-
></path>
|
|
544
|
-
</svg>
|
|
545
|
-
</button>
|
|
546
|
-
</div>
|
|
547
|
-
{/if}
|
|
548
|
-
|
|
549
|
-
{#if isOpen || inline}
|
|
550
|
-
<div
|
|
551
|
-
bind:this={calendarRef}
|
|
552
|
-
id="datepicker-dropdown"
|
|
553
|
-
class={base({ inline, class: clsx(theme?.base, className) })}
|
|
554
|
-
transition:fade={{ duration: 100 }}
|
|
555
|
-
role="dialog"
|
|
556
|
-
aria-label="Calendar"
|
|
557
|
-
>
|
|
558
|
-
{#if title}
|
|
559
|
-
<h2 class={titleVariant({ class: clsx(theme?.titleVariant, classes?.titleVariant) })}>
|
|
560
|
-
{title}
|
|
561
|
-
</h2>
|
|
562
|
-
{/if}
|
|
563
|
-
|
|
564
|
-
{#if showMonthSelector}
|
|
565
|
-
<!-- Month/Year Selector View -->
|
|
566
|
-
<div class={nav({ class: clsx(theme?.nav, classes?.nav) })}>
|
|
567
|
-
{@render yearNavButton(false)}
|
|
568
|
-
<h3 class={polite({ class: clsx(theme?.polite, classes?.polite) })} aria-live="polite">
|
|
569
|
-
{currentMonth.getFullYear()}
|
|
570
|
-
</h3>
|
|
571
|
-
{@render yearNavButton(true)}
|
|
572
|
-
</div>
|
|
573
|
-
<div class="grid grid-cols-4 gap-2 p-4">
|
|
574
|
-
{#each monthNames as month, index}
|
|
575
|
-
<Button
|
|
576
|
-
type="button"
|
|
577
|
-
color={monthColor}
|
|
578
|
-
class={monthButton({
|
|
579
|
-
class: clsx(
|
|
580
|
-
currentMonth.getMonth() === index ? monthBtnSelected : monthBtn,
|
|
581
|
-
classes?.monthButton,
|
|
582
|
-
theme?.monthButton
|
|
583
|
-
)
|
|
584
|
-
})}
|
|
585
|
-
onclick={(event: MouseEvent) => selectMonth(index, event)}
|
|
586
|
-
>
|
|
587
|
-
{month}
|
|
588
|
-
</Button>
|
|
589
|
-
{/each}
|
|
590
|
-
</div>
|
|
591
|
-
{:else}
|
|
592
|
-
<div class={nav({ class: clsx(classes?.nav) })}>
|
|
593
|
-
{@render navButton(false)}
|
|
594
|
-
<Button
|
|
595
|
-
type="button"
|
|
596
|
-
class={polite({
|
|
597
|
-
class: clsx(
|
|
598
|
-
'cursor-pointer rounded px-2 py-1 hover:bg-gray-100 dark:hover:bg-gray-700',
|
|
599
|
-
classes?.polite
|
|
600
|
-
)
|
|
601
|
-
})}
|
|
602
|
-
aria-live="polite"
|
|
603
|
-
onclick={(event: MouseEvent) => toggleMonthSelector(event)}
|
|
604
|
-
>
|
|
605
|
-
{currentMonth.toLocaleString(translationLocale, { month: 'long', year: 'numeric' })}
|
|
606
|
-
</Button>
|
|
607
|
-
{@render navButton(true)}
|
|
608
|
-
</div>
|
|
609
|
-
<div class={grid({ class: clsx(theme?.grid, classes?.grid) })} role="grid">
|
|
610
|
-
{#each weekdays as day}
|
|
611
|
-
<div
|
|
612
|
-
class={columnHeader({ class: clsx(theme?.columnHeader, classes?.columnHeader) })}
|
|
613
|
-
role="columnheader"
|
|
614
|
-
>
|
|
615
|
-
{day}
|
|
616
|
-
</div>
|
|
617
|
-
{/each}
|
|
618
|
-
{#each daysInMonth as day}
|
|
619
|
-
{@const current = day.getMonth() !== currentMonth.getMonth()}
|
|
620
|
-
{@const available = isDateAvailable(day)}
|
|
621
|
-
<Button
|
|
622
|
-
type="button"
|
|
623
|
-
color={isSelected(day) ? color : 'alternative'}
|
|
624
|
-
class={dayButton({
|
|
625
|
-
current,
|
|
626
|
-
today: isToday(day),
|
|
627
|
-
color: isInRange(day) ? color : undefined,
|
|
628
|
-
unavailable: !available,
|
|
629
|
-
class: clsx(
|
|
630
|
-
theme?.dayButton,
|
|
631
|
-
classes?.dayButton,
|
|
632
|
-
!available && 'cursor-not-allowed opacity-50'
|
|
633
|
-
)
|
|
634
|
-
})}
|
|
635
|
-
onclick={() => handleDaySelect(day)}
|
|
636
|
-
onkeydown={handleCalendarKeydown}
|
|
637
|
-
aria-label={day.toLocaleDateString(translationLocale, {
|
|
638
|
-
weekday: 'long',
|
|
639
|
-
year: 'numeric',
|
|
640
|
-
month: 'long',
|
|
641
|
-
day: 'numeric'
|
|
642
|
-
})}
|
|
643
|
-
aria-selected={isSelected(day)}
|
|
644
|
-
aria-disabled={!available}
|
|
645
|
-
disabled={!available}
|
|
646
|
-
role="gridcell"
|
|
647
|
-
>
|
|
648
|
-
{day.getDate()}
|
|
649
|
-
</Button>
|
|
650
|
-
{/each}
|
|
651
|
-
</div>
|
|
652
|
-
{/if}
|
|
653
|
-
|
|
654
|
-
{#if showActionButtons && !showMonthSelector}
|
|
655
|
-
<div class={actionButtons({ class: clsx(theme?.actionButtons, classes?.actionButtons) })}>
|
|
656
|
-
<Button
|
|
657
|
-
onclick={() => handleDaySelect(new Date())}
|
|
658
|
-
{color}
|
|
659
|
-
size="sm"
|
|
660
|
-
disabled={!isDateAvailable(new Date())}>Today</Button
|
|
661
|
-
>
|
|
662
|
-
<Button onclick={handleClear} color="red" size="sm">Clear</Button>
|
|
663
|
-
<Button onclick={handleApply} {color} size="sm">Apply</Button>
|
|
664
|
-
</div>
|
|
665
|
-
{/if}
|
|
666
|
-
</div>
|
|
667
|
-
{/if}
|
|
668
|
-
</div>
|
|
669
|
-
|
|
670
|
-
<!--
|
|
671
|
-
@component
|
|
672
|
-
[Go to docs](https://flowbite-svelte.com/)
|
|
673
|
-
## Type
|
|
674
|
-
[DatepickerProps](https://github.com/themesberg/flowbite-svelte/blob/main/src/lib/types.ts#L449)
|
|
675
|
-
## Props
|
|
676
|
-
@prop value = $bindable()
|
|
677
|
-
@prop defaultDate = null
|
|
678
|
-
@prop range = false
|
|
679
|
-
@prop rangeFrom = $bindable()
|
|
680
|
-
@prop rangeTo = $bindable()
|
|
681
|
-
@prop availableFrom = null
|
|
682
|
-
@prop availableTo = null
|
|
683
|
-
@prop locale = "default"
|
|
684
|
-
@prop translationLocale = locale
|
|
685
|
-
@prop firstDayOfWeek = 0
|
|
686
|
-
@prop dateFormat
|
|
687
|
-
@prop placeholder = "Select date"
|
|
688
|
-
@prop disabled = false
|
|
689
|
-
@prop required = false
|
|
690
|
-
@prop inputClass = ""
|
|
691
|
-
@prop color = "primary"
|
|
692
|
-
@prop inline = false
|
|
693
|
-
@prop autohide = true
|
|
694
|
-
@prop showActionButtons = false
|
|
695
|
-
@prop title = ""
|
|
696
|
-
@prop onselect
|
|
697
|
-
@prop onclear
|
|
698
|
-
@prop onapply
|
|
699
|
-
@prop btnClass
|
|
700
|
-
@prop inputmode = "none"
|
|
701
|
-
@prop classes
|
|
702
|
-
@prop monthColor = "alternative"
|
|
703
|
-
@prop monthBtnSelected = "bg-primary-500 text-white"
|
|
704
|
-
@prop monthBtn = "text-gray-700 dark:text-gray-300"
|
|
705
|
-
@prop class: className
|
|
706
|
-
@prop elementRef = $bindable()
|
|
707
|
-
-->
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import { fade } from 'svelte/transition';
|
|
4
|
+
import clsx from 'clsx';
|
|
5
|
+
import { datepicker } from './index.js';
|
|
6
|
+
import {
|
|
7
|
+
parse,
|
|
8
|
+
isValid,
|
|
9
|
+
addDays,
|
|
10
|
+
startOfMonth,
|
|
11
|
+
endOfMonth,
|
|
12
|
+
startOfWeek,
|
|
13
|
+
endOfWeek,
|
|
14
|
+
eachDayOfInterval,
|
|
15
|
+
isSameDay,
|
|
16
|
+
isWithinInterval
|
|
17
|
+
} from 'date-fns';
|
|
18
|
+
import { Button, getTheme, ToolbarButton, type DatepickerProps } from '../../index.js';
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
value = $bindable(),
|
|
22
|
+
defaultDate = null,
|
|
23
|
+
range = false,
|
|
24
|
+
rangeFrom = $bindable(),
|
|
25
|
+
rangeTo = $bindable(),
|
|
26
|
+
availableFrom = null,
|
|
27
|
+
availableTo = null,
|
|
28
|
+
locale = 'default',
|
|
29
|
+
translationLocale = locale,
|
|
30
|
+
firstDayOfWeek = 0,
|
|
31
|
+
dateFormat,
|
|
32
|
+
placeholder = 'Select date',
|
|
33
|
+
disabled = false,
|
|
34
|
+
required = false,
|
|
35
|
+
inputClass = '',
|
|
36
|
+
color = 'primary',
|
|
37
|
+
inline = false,
|
|
38
|
+
autohide = true,
|
|
39
|
+
showActionButtons = false,
|
|
40
|
+
title = '',
|
|
41
|
+
onselect,
|
|
42
|
+
onclear,
|
|
43
|
+
onapply,
|
|
44
|
+
btnClass,
|
|
45
|
+
inputmode = 'none',
|
|
46
|
+
classes,
|
|
47
|
+
monthColor = 'alternative',
|
|
48
|
+
monthBtnSelected = 'bg-primary-500 text-white',
|
|
49
|
+
monthBtn = 'text-gray-700 dark:text-gray-300',
|
|
50
|
+
class: className,
|
|
51
|
+
elementRef = $bindable(),
|
|
52
|
+
name,
|
|
53
|
+
// New placement prop
|
|
54
|
+
placement = 'bottom-start'
|
|
55
|
+
}: DatepickerProps = $props();
|
|
56
|
+
|
|
57
|
+
const theme = getTheme('datepicker');
|
|
58
|
+
|
|
59
|
+
// If translationLocale is not explicitly provided, it will default to the value of locale. This ensures reactivity as both are directly exposed as props.
|
|
60
|
+
translationLocale = translationLocale ?? locale;
|
|
61
|
+
|
|
62
|
+
let isOpen: boolean = $state(inline);
|
|
63
|
+
let showMonthSelector: boolean = $state(false);
|
|
64
|
+
let inputElement: HTMLInputElement | null = $state(null);
|
|
65
|
+
|
|
66
|
+
$effect(() => {
|
|
67
|
+
if (inputElement) {
|
|
68
|
+
elementRef = inputElement;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
let datepickerContainerElement: HTMLDivElement;
|
|
72
|
+
let currentMonth: Date = $state(value || defaultDate || new Date());
|
|
73
|
+
let focusedDate: Date | null = null;
|
|
74
|
+
let calendarRef: HTMLDivElement | null = $state(null);
|
|
75
|
+
|
|
76
|
+
let daysInMonth = $derived(getDaysInMonth(currentMonth));
|
|
77
|
+
|
|
78
|
+
onMount(() => {
|
|
79
|
+
if (!inline) {
|
|
80
|
+
datepickerContainerElement?.ownerDocument.addEventListener('click', handleClickOutside);
|
|
81
|
+
return () => {
|
|
82
|
+
datepickerContainerElement?.ownerDocument.removeEventListener('click', handleClickOutside);
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
function getDaysInMonth(date: Date): Date[] {
|
|
88
|
+
const monthStart = startOfMonth(date);
|
|
89
|
+
const monthEnd = endOfMonth(date);
|
|
90
|
+
const calendarStart = startOfWeek(monthStart, { weekStartsOn: firstDayOfWeek });
|
|
91
|
+
const calendarEnd = endOfWeek(monthEnd, { weekStartsOn: firstDayOfWeek });
|
|
92
|
+
|
|
93
|
+
return eachDayOfInterval({ start: calendarStart, end: calendarEnd });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const getWeekdayNames = (): string[] => {
|
|
97
|
+
const referenceDate = new Date(1970, 0, 4 + firstDayOfWeek);
|
|
98
|
+
return Array.from({ length: 7 }, (_, i) =>
|
|
99
|
+
addDays(referenceDate, i).toLocaleDateString(translationLocale, { weekday: 'short' })
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
let weekdays = $derived(getWeekdayNames());
|
|
104
|
+
|
|
105
|
+
const getMonthNames = (): string[] => {
|
|
106
|
+
return Array.from({ length: 12 }, (_, i) =>
|
|
107
|
+
new Date(2000, i, 1).toLocaleDateString(translationLocale, { month: 'short' })
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
let monthNames = $derived(getMonthNames());
|
|
111
|
+
|
|
112
|
+
const addDay = (date: Date, increment: number): Date => addDays(date, increment);
|
|
113
|
+
|
|
114
|
+
function changeMonth(increment: number) {
|
|
115
|
+
currentMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + increment, 1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function changeYear(increment: number) {
|
|
119
|
+
currentMonth = new Date(currentMonth.getFullYear() + increment, currentMonth.getMonth(), 1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function selectMonth(monthIndex: number, event: MouseEvent) {
|
|
123
|
+
event.stopPropagation();
|
|
124
|
+
currentMonth = new Date(currentMonth.getFullYear(), monthIndex, 1);
|
|
125
|
+
showMonthSelector = false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function toggleMonthSelector(event: MouseEvent) {
|
|
129
|
+
event.stopPropagation();
|
|
130
|
+
showMonthSelector = !showMonthSelector;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isDateAvailable(date: Date): boolean {
|
|
134
|
+
const dateOnly = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
135
|
+
|
|
136
|
+
if (availableFrom) {
|
|
137
|
+
const fromDate = new Date(
|
|
138
|
+
availableFrom.getFullYear(),
|
|
139
|
+
availableFrom.getMonth(),
|
|
140
|
+
availableFrom.getDate()
|
|
141
|
+
);
|
|
142
|
+
if (dateOnly < fromDate) return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (availableTo) {
|
|
146
|
+
const toDate = new Date(
|
|
147
|
+
availableTo.getFullYear(),
|
|
148
|
+
availableTo.getMonth(),
|
|
149
|
+
availableTo.getDate()
|
|
150
|
+
);
|
|
151
|
+
if (dateOnly > toDate) return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function handleDaySelect(day: Date) {
|
|
158
|
+
if (!isDateAvailable(day)) return;
|
|
159
|
+
|
|
160
|
+
if (range) {
|
|
161
|
+
if (!rangeFrom || (rangeFrom && rangeTo)) {
|
|
162
|
+
rangeFrom = day;
|
|
163
|
+
rangeTo = undefined;
|
|
164
|
+
} else if (day < rangeFrom) {
|
|
165
|
+
rangeFrom = day;
|
|
166
|
+
rangeTo = rangeFrom;
|
|
167
|
+
} else {
|
|
168
|
+
rangeTo = day;
|
|
169
|
+
}
|
|
170
|
+
onselect?.({ from: rangeFrom, to: rangeTo });
|
|
171
|
+
} else {
|
|
172
|
+
value = day;
|
|
173
|
+
onselect?.(value);
|
|
174
|
+
if (autohide && !inline) isOpen = false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function handleInputChangeWithDateFns() {
|
|
179
|
+
const inputValue = inputElement?.value?.trim();
|
|
180
|
+
if (!inputValue) {
|
|
181
|
+
rangeFrom = undefined;
|
|
182
|
+
rangeTo = undefined;
|
|
183
|
+
inputElement?.setCustomValidity('');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
inputElement?.setCustomValidity('');
|
|
188
|
+
|
|
189
|
+
if (range) {
|
|
190
|
+
const parts = inputValue.split(' - ');
|
|
191
|
+
if (parts.length === 2) {
|
|
192
|
+
const parsedFrom = tryParseDate(parts[0]);
|
|
193
|
+
const parsedTo = tryParseDate(parts[1]);
|
|
194
|
+
|
|
195
|
+
if (
|
|
196
|
+
parsedFrom &&
|
|
197
|
+
isValid(parsedFrom) &&
|
|
198
|
+
isDateAvailable(parsedFrom) &&
|
|
199
|
+
parsedTo &&
|
|
200
|
+
isValid(parsedTo) &&
|
|
201
|
+
isDateAvailable(parsedTo)
|
|
202
|
+
) {
|
|
203
|
+
[rangeFrom, rangeTo] =
|
|
204
|
+
parsedFrom > parsedTo ? [parsedTo, parsedFrom] : [parsedFrom, parsedTo];
|
|
205
|
+
onselect?.({ from: rangeFrom, to: rangeTo });
|
|
206
|
+
return;
|
|
207
|
+
} else {
|
|
208
|
+
inputElement?.setCustomValidity(
|
|
209
|
+
`Please enter date range in format: ${getDateFormatPattern()} - ${getDateFormatPattern()}`
|
|
210
|
+
);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const parsedDate = tryParseDate(inputValue);
|
|
217
|
+
|
|
218
|
+
if (!parsedDate || !isValid(parsedDate)) {
|
|
219
|
+
const formatPattern = getDateFormatPattern();
|
|
220
|
+
inputElement?.setCustomValidity(`Please enter date in format: ${formatPattern}`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!isDateAvailable(parsedDate)) {
|
|
225
|
+
inputElement?.setCustomValidity('Selected date is not available');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
handleDaySelect(parsedDate);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function tryParseDate(inputValue: string): Date | null {
|
|
233
|
+
const formatPattern = getDateFormatPattern();
|
|
234
|
+
try {
|
|
235
|
+
const parsedDate = parse(inputValue, formatPattern, new Date());
|
|
236
|
+
if (isValid(parsedDate)) {
|
|
237
|
+
return parsedDate;
|
|
238
|
+
}
|
|
239
|
+
} catch (error) {
|
|
240
|
+
// Continue to next strategy
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const commonFormats = [
|
|
244
|
+
'd.M.yyyy', // German: 17.7.2025
|
|
245
|
+
'dd.MM.yyyy', // German: 17.07.2025
|
|
246
|
+
'M/d/yyyy', // US: 7/17/2025
|
|
247
|
+
'MM/dd/yyyy', // US: 07/17/2025
|
|
248
|
+
'd/M/yyyy', // UK: 17/7/2025
|
|
249
|
+
'dd/MM/yyyy', // UK: 17/07/2025
|
|
250
|
+
'yyyy-MM-dd', // ISO: 2025-07-17
|
|
251
|
+
'yyyy-M-d', // ISO: 2025-7-17
|
|
252
|
+
'M-d-yyyy', // US with dashes: 7-17-2025
|
|
253
|
+
'd-M-yyyy' // EU with dashes: 17-7-2025
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
for (const format of commonFormats) {
|
|
257
|
+
try {
|
|
258
|
+
const parsedDate = parse(inputValue, format, new Date());
|
|
259
|
+
if (isValid(parsedDate)) {
|
|
260
|
+
return parsedDate;
|
|
261
|
+
}
|
|
262
|
+
} catch (error) {
|
|
263
|
+
// Continue to next format
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const nativeDate = new Date(inputValue);
|
|
269
|
+
if (isValid(nativeDate) && !isNaN(nativeDate.getTime())) {
|
|
270
|
+
return nativeDate;
|
|
271
|
+
}
|
|
272
|
+
} catch (error) {
|
|
273
|
+
// Continue
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function getDateFormatPattern(): string {
|
|
280
|
+
const actualLocale = locale === 'default' ? navigator.language : locale;
|
|
281
|
+
const testDate = new Date(2025, 0, 15); // January 15, 2025
|
|
282
|
+
const formatted = testDate.toLocaleDateString(
|
|
283
|
+
actualLocale,
|
|
284
|
+
dateFormat || { year: 'numeric', month: 'numeric', day: 'numeric' }
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
if (formatted.includes('.')) {
|
|
288
|
+
// German/European format with dots
|
|
289
|
+
if (formatted.startsWith('15.')) {
|
|
290
|
+
return 'd.M.yyyy';
|
|
291
|
+
} else if (formatted.startsWith('01.')) {
|
|
292
|
+
return 'M.d.yyyy';
|
|
293
|
+
}
|
|
294
|
+
return 'd.M.yyyy'; // Default to day first
|
|
295
|
+
} else if (formatted.includes('/')) {
|
|
296
|
+
// US/UK format with slashes
|
|
297
|
+
if (formatted.startsWith('1/')) {
|
|
298
|
+
return 'M/d/yyyy'; // US format
|
|
299
|
+
} else if (formatted.startsWith('15/')) {
|
|
300
|
+
return 'd/M/yyyy'; // UK format
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const testDate2 = new Date(2025, 11, 3); // December 3, 2025
|
|
304
|
+
const formatted2 = testDate2.toLocaleDateString(
|
|
305
|
+
actualLocale,
|
|
306
|
+
dateFormat || { year: 'numeric', month: 'numeric', day: 'numeric' }
|
|
307
|
+
);
|
|
308
|
+
if (formatted2.startsWith('3/') || formatted2.startsWith('03/')) {
|
|
309
|
+
return 'd/M/yyyy';
|
|
310
|
+
} else {
|
|
311
|
+
return 'M/d/yyyy';
|
|
312
|
+
}
|
|
313
|
+
} else if (formatted.includes('-')) {
|
|
314
|
+
// ISO or other dash format
|
|
315
|
+
if (formatted.startsWith('2025-')) {
|
|
316
|
+
return 'yyyy-M-d';
|
|
317
|
+
} else if (formatted.startsWith('1-')) {
|
|
318
|
+
return 'M-d-yyyy';
|
|
319
|
+
} else {
|
|
320
|
+
return 'd-M-yyyy';
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Default fallback - try to detect based on locale
|
|
325
|
+
if (actualLocale.startsWith('en-US')) {
|
|
326
|
+
return 'M/d/yyyy';
|
|
327
|
+
} else if (
|
|
328
|
+
actualLocale.startsWith('de') ||
|
|
329
|
+
actualLocale.startsWith('at') ||
|
|
330
|
+
actualLocale.startsWith('ch')
|
|
331
|
+
) {
|
|
332
|
+
return 'd.M.yyyy';
|
|
333
|
+
} else if (actualLocale.startsWith('en-GB') || actualLocale.startsWith('en-AU')) {
|
|
334
|
+
return 'd/M/yyyy';
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return 'M/d/yyyy';
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function handleClickOutside(event: MouseEvent) {
|
|
341
|
+
if (
|
|
342
|
+
isOpen &&
|
|
343
|
+
datepickerContainerElement &&
|
|
344
|
+
!datepickerContainerElement.contains(event.target as Node)
|
|
345
|
+
) {
|
|
346
|
+
isOpen = false;
|
|
347
|
+
showMonthSelector = false;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Use locale for formatting (not translationLocale)
|
|
352
|
+
const formatDate = (date?: Date): string => date?.toLocaleDateString(locale, dateFormat) ?? '';
|
|
353
|
+
const isSameDate = (date1?: Date, date2?: Date): boolean =>
|
|
354
|
+
date1 && date2 ? isSameDay(date1, date2) : false;
|
|
355
|
+
const isToday = (day: Date): boolean => isSameDate(day, new Date());
|
|
356
|
+
const isInRange = (day: Date): boolean =>
|
|
357
|
+
!!(range && rangeFrom && rangeTo && isWithinInterval(day, { start: rangeFrom, end: rangeTo }));
|
|
358
|
+
|
|
359
|
+
let isSelected = $derived((day: Date): boolean =>
|
|
360
|
+
range ? isSameDate(day, rangeFrom) || isSameDate(day, rangeTo) : isSameDate(day, value)
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
function handleCalendarKeydown(event: KeyboardEvent) {
|
|
364
|
+
if (!isOpen) return;
|
|
365
|
+
|
|
366
|
+
if (!focusedDate) {
|
|
367
|
+
focusedDate = value || new Date();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
switch (event.key) {
|
|
371
|
+
case 'ArrowLeft':
|
|
372
|
+
focusedDate = addDay(focusedDate, -1);
|
|
373
|
+
break;
|
|
374
|
+
case 'ArrowRight':
|
|
375
|
+
focusedDate = addDay(focusedDate, 1);
|
|
376
|
+
break;
|
|
377
|
+
case 'ArrowUp':
|
|
378
|
+
focusedDate = addDay(focusedDate, -7);
|
|
379
|
+
break;
|
|
380
|
+
case 'ArrowDown':
|
|
381
|
+
focusedDate = addDay(focusedDate, 7);
|
|
382
|
+
break;
|
|
383
|
+
case 'Enter':
|
|
384
|
+
if (range) {
|
|
385
|
+
if (rangeFrom && rangeTo) {
|
|
386
|
+
if (autohide && !inline) isOpen = false;
|
|
387
|
+
} else {
|
|
388
|
+
handleDaySelect(focusedDate);
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
handleDaySelect(focusedDate);
|
|
392
|
+
if (autohide && !inline) isOpen = false;
|
|
393
|
+
}
|
|
394
|
+
break;
|
|
395
|
+
case 'Escape':
|
|
396
|
+
isOpen = false;
|
|
397
|
+
showMonthSelector = false;
|
|
398
|
+
inputElement?.focus();
|
|
399
|
+
break;
|
|
400
|
+
default:
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
event.preventDefault();
|
|
405
|
+
if (focusedDate.getMonth() !== currentMonth.getMonth()) {
|
|
406
|
+
currentMonth = new Date(focusedDate.getFullYear(), focusedDate.getMonth(), 1);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Use translationLocale for aria-label
|
|
410
|
+
setTimeout(() => {
|
|
411
|
+
const focusedButton = calendarRef?.querySelector(
|
|
412
|
+
`button[aria-label="${focusedDate!.toLocaleDateString(translationLocale, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}"]`
|
|
413
|
+
) as HTMLButtonElement | null;
|
|
414
|
+
focusedButton?.focus();
|
|
415
|
+
}, 0);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function handleInputKeydown(event: KeyboardEvent) {
|
|
419
|
+
if (event.key === 'Enter') {
|
|
420
|
+
event.preventDefault();
|
|
421
|
+
handleInputChangeWithDateFns();
|
|
422
|
+
if (autohide && !inline) {
|
|
423
|
+
isOpen = false;
|
|
424
|
+
}
|
|
425
|
+
} else if (event.key === ' ') {
|
|
426
|
+
event.preventDefault();
|
|
427
|
+
isOpen = !isOpen;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function handleClear() {
|
|
432
|
+
value = rangeFrom = rangeTo = undefined;
|
|
433
|
+
onclear?.();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function handleApply() {
|
|
437
|
+
const result = range ? { from: rangeFrom, to: rangeTo } : value;
|
|
438
|
+
if (result) onapply?.(result);
|
|
439
|
+
if (!inline) isOpen = false;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
let {
|
|
443
|
+
base,
|
|
444
|
+
input,
|
|
445
|
+
button,
|
|
446
|
+
titleVariant,
|
|
447
|
+
actionButtons,
|
|
448
|
+
columnHeader,
|
|
449
|
+
polite,
|
|
450
|
+
grid,
|
|
451
|
+
nav,
|
|
452
|
+
dayButton,
|
|
453
|
+
monthButton
|
|
454
|
+
} = datepicker({ placement });
|
|
455
|
+
</script>
|
|
456
|
+
|
|
457
|
+
{#snippet navButton(forward: boolean)}
|
|
458
|
+
<ToolbarButton
|
|
459
|
+
color="dark"
|
|
460
|
+
onclick={() => changeMonth(forward ? 1 : -1)}
|
|
461
|
+
size="lg"
|
|
462
|
+
aria-label={forward ? 'Next month' : 'Previous month'}
|
|
463
|
+
>
|
|
464
|
+
<svg
|
|
465
|
+
class="h-3 w-3 rtl:rotate-180"
|
|
466
|
+
aria-hidden="true"
|
|
467
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
468
|
+
fill="none"
|
|
469
|
+
viewBox="0 0 14 10"
|
|
470
|
+
>
|
|
471
|
+
<path
|
|
472
|
+
stroke="currentColor"
|
|
473
|
+
stroke-linecap="round"
|
|
474
|
+
stroke-linejoin="round"
|
|
475
|
+
stroke-width="2"
|
|
476
|
+
d={forward ? 'M1 5h12m0 0L9 1m4 4L9 9' : 'M13 5H1m0 0 4 4M1 5l4-4'}
|
|
477
|
+
></path>
|
|
478
|
+
</svg>
|
|
479
|
+
</ToolbarButton>
|
|
480
|
+
{/snippet}
|
|
481
|
+
|
|
482
|
+
{#snippet yearNavButton(forward: boolean)}
|
|
483
|
+
<ToolbarButton
|
|
484
|
+
color="dark"
|
|
485
|
+
onclick={() => changeYear(forward ? 1 : -1)}
|
|
486
|
+
size="lg"
|
|
487
|
+
aria-label={forward ? 'Next year' : 'Previous year'}
|
|
488
|
+
>
|
|
489
|
+
<svg
|
|
490
|
+
class="h-3 w-3 rtl:rotate-180"
|
|
491
|
+
aria-hidden="true"
|
|
492
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
493
|
+
fill="none"
|
|
494
|
+
viewBox="0 0 14 10"
|
|
495
|
+
>
|
|
496
|
+
<path
|
|
497
|
+
stroke="currentColor"
|
|
498
|
+
stroke-linecap="round"
|
|
499
|
+
stroke-linejoin="round"
|
|
500
|
+
stroke-width="2"
|
|
501
|
+
d={forward ? 'M1 5h12m0 0L9 1m4 4L9 9' : 'M13 5H1m0 0 4 4M1 5l4-4'}
|
|
502
|
+
></path>
|
|
503
|
+
</svg>
|
|
504
|
+
</ToolbarButton>
|
|
505
|
+
{/snippet}
|
|
506
|
+
|
|
507
|
+
<div bind:this={datepickerContainerElement} class={['relative', inline && 'inline-block']}>
|
|
508
|
+
{#if !inline}
|
|
509
|
+
<div class="relative">
|
|
510
|
+
<input
|
|
511
|
+
bind:this={inputElement}
|
|
512
|
+
type="text"
|
|
513
|
+
class={input({ color, class: clsx(theme?.input, inputClass) })}
|
|
514
|
+
{placeholder}
|
|
515
|
+
value={range && rangeFrom
|
|
516
|
+
? `${formatDate(rangeFrom)} - ${formatDate(rangeTo)}`
|
|
517
|
+
: formatDate(value)}
|
|
518
|
+
onfocus={() => (isOpen = true)}
|
|
519
|
+
onchange={handleInputChangeWithDateFns}
|
|
520
|
+
onkeydown={handleInputKeydown}
|
|
521
|
+
{disabled}
|
|
522
|
+
{required}
|
|
523
|
+
{inputmode}
|
|
524
|
+
aria-haspopup="dialog"
|
|
525
|
+
{name}
|
|
526
|
+
/>
|
|
527
|
+
<button
|
|
528
|
+
type="button"
|
|
529
|
+
class={button({ class: clsx(btnClass, theme?.button, classes?.button) })}
|
|
530
|
+
onclick={() => (isOpen = !isOpen)}
|
|
531
|
+
{disabled}
|
|
532
|
+
aria-label={isOpen ? 'Close date picker' : 'Open date picker'}
|
|
533
|
+
>
|
|
534
|
+
<svg
|
|
535
|
+
class="h-4 w-4 text-gray-500 dark:text-gray-400"
|
|
536
|
+
aria-hidden="true"
|
|
537
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
538
|
+
fill="currentColor"
|
|
539
|
+
viewBox="0 0 20 20"
|
|
540
|
+
>
|
|
541
|
+
<path
|
|
542
|
+
d="M20 4a2 2 0 0 0-2-2h-2V1a1 1 0 0 0-2 0v1h-3V1a1 1 0 0 0-2 0v1H6V1a1 1 0 0 0-2 0v1H2a2 2 0 0 0-2 2v2h20V4ZM0 18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8H0v10Zm5-8h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2Z"
|
|
543
|
+
></path>
|
|
544
|
+
</svg>
|
|
545
|
+
</button>
|
|
546
|
+
</div>
|
|
547
|
+
{/if}
|
|
548
|
+
|
|
549
|
+
{#if isOpen || inline}
|
|
550
|
+
<div
|
|
551
|
+
bind:this={calendarRef}
|
|
552
|
+
id="datepicker-dropdown"
|
|
553
|
+
class={base({ inline, class: clsx(theme?.base, className) })}
|
|
554
|
+
transition:fade={{ duration: 100 }}
|
|
555
|
+
role="dialog"
|
|
556
|
+
aria-label="Calendar"
|
|
557
|
+
>
|
|
558
|
+
{#if title}
|
|
559
|
+
<h2 class={titleVariant({ class: clsx(theme?.titleVariant, classes?.titleVariant) })}>
|
|
560
|
+
{title}
|
|
561
|
+
</h2>
|
|
562
|
+
{/if}
|
|
563
|
+
|
|
564
|
+
{#if showMonthSelector}
|
|
565
|
+
<!-- Month/Year Selector View -->
|
|
566
|
+
<div class={nav({ class: clsx(theme?.nav, classes?.nav) })}>
|
|
567
|
+
{@render yearNavButton(false)}
|
|
568
|
+
<h3 class={polite({ class: clsx(theme?.polite, classes?.polite) })} aria-live="polite">
|
|
569
|
+
{currentMonth.getFullYear()}
|
|
570
|
+
</h3>
|
|
571
|
+
{@render yearNavButton(true)}
|
|
572
|
+
</div>
|
|
573
|
+
<div class="grid grid-cols-4 gap-2 p-4">
|
|
574
|
+
{#each monthNames as month, index}
|
|
575
|
+
<Button
|
|
576
|
+
type="button"
|
|
577
|
+
color={monthColor}
|
|
578
|
+
class={monthButton({
|
|
579
|
+
class: clsx(
|
|
580
|
+
currentMonth.getMonth() === index ? monthBtnSelected : monthBtn,
|
|
581
|
+
classes?.monthButton,
|
|
582
|
+
theme?.monthButton
|
|
583
|
+
)
|
|
584
|
+
})}
|
|
585
|
+
onclick={(event: MouseEvent) => selectMonth(index, event)}
|
|
586
|
+
>
|
|
587
|
+
{month}
|
|
588
|
+
</Button>
|
|
589
|
+
{/each}
|
|
590
|
+
</div>
|
|
591
|
+
{:else}
|
|
592
|
+
<div class={nav({ class: clsx(classes?.nav) })}>
|
|
593
|
+
{@render navButton(false)}
|
|
594
|
+
<Button
|
|
595
|
+
type="button"
|
|
596
|
+
class={polite({
|
|
597
|
+
class: clsx(
|
|
598
|
+
'cursor-pointer rounded px-2 py-1 hover:bg-gray-100 dark:hover:bg-gray-700',
|
|
599
|
+
classes?.polite
|
|
600
|
+
)
|
|
601
|
+
})}
|
|
602
|
+
aria-live="polite"
|
|
603
|
+
onclick={(event: MouseEvent) => toggleMonthSelector(event)}
|
|
604
|
+
>
|
|
605
|
+
{currentMonth.toLocaleString(translationLocale, { month: 'long', year: 'numeric' })}
|
|
606
|
+
</Button>
|
|
607
|
+
{@render navButton(true)}
|
|
608
|
+
</div>
|
|
609
|
+
<div class={grid({ class: clsx(theme?.grid, classes?.grid) })} role="grid">
|
|
610
|
+
{#each weekdays as day}
|
|
611
|
+
<div
|
|
612
|
+
class={columnHeader({ class: clsx(theme?.columnHeader, classes?.columnHeader) })}
|
|
613
|
+
role="columnheader"
|
|
614
|
+
>
|
|
615
|
+
{day}
|
|
616
|
+
</div>
|
|
617
|
+
{/each}
|
|
618
|
+
{#each daysInMonth as day}
|
|
619
|
+
{@const current = day.getMonth() !== currentMonth.getMonth()}
|
|
620
|
+
{@const available = isDateAvailable(day)}
|
|
621
|
+
<Button
|
|
622
|
+
type="button"
|
|
623
|
+
color={isSelected(day) ? color : 'alternative'}
|
|
624
|
+
class={dayButton({
|
|
625
|
+
current,
|
|
626
|
+
today: isToday(day),
|
|
627
|
+
color: isInRange(day) ? color : undefined,
|
|
628
|
+
unavailable: !available,
|
|
629
|
+
class: clsx(
|
|
630
|
+
theme?.dayButton,
|
|
631
|
+
classes?.dayButton,
|
|
632
|
+
!available && 'cursor-not-allowed opacity-50'
|
|
633
|
+
)
|
|
634
|
+
})}
|
|
635
|
+
onclick={() => handleDaySelect(day)}
|
|
636
|
+
onkeydown={handleCalendarKeydown}
|
|
637
|
+
aria-label={day.toLocaleDateString(translationLocale, {
|
|
638
|
+
weekday: 'long',
|
|
639
|
+
year: 'numeric',
|
|
640
|
+
month: 'long',
|
|
641
|
+
day: 'numeric'
|
|
642
|
+
})}
|
|
643
|
+
aria-selected={isSelected(day)}
|
|
644
|
+
aria-disabled={!available}
|
|
645
|
+
disabled={!available}
|
|
646
|
+
role="gridcell"
|
|
647
|
+
>
|
|
648
|
+
{day.getDate()}
|
|
649
|
+
</Button>
|
|
650
|
+
{/each}
|
|
651
|
+
</div>
|
|
652
|
+
{/if}
|
|
653
|
+
|
|
654
|
+
{#if showActionButtons && !showMonthSelector}
|
|
655
|
+
<div class={actionButtons({ class: clsx(theme?.actionButtons, classes?.actionButtons) })}>
|
|
656
|
+
<Button
|
|
657
|
+
onclick={() => handleDaySelect(new Date())}
|
|
658
|
+
{color}
|
|
659
|
+
size="sm"
|
|
660
|
+
disabled={!isDateAvailable(new Date())}>Today</Button
|
|
661
|
+
>
|
|
662
|
+
<Button onclick={handleClear} color="red" size="sm">Clear</Button>
|
|
663
|
+
<Button onclick={handleApply} {color} size="sm">Apply</Button>
|
|
664
|
+
</div>
|
|
665
|
+
{/if}
|
|
666
|
+
</div>
|
|
667
|
+
{/if}
|
|
668
|
+
</div>
|
|
669
|
+
|
|
670
|
+
<!--
|
|
671
|
+
@component
|
|
672
|
+
[Go to docs](https://flowbite-svelte.com/)
|
|
673
|
+
## Type
|
|
674
|
+
[DatepickerProps](https://github.com/themesberg/flowbite-svelte/blob/main/src/lib/types.ts#L449)
|
|
675
|
+
## Props
|
|
676
|
+
@prop value = $bindable()
|
|
677
|
+
@prop defaultDate = null
|
|
678
|
+
@prop range = false
|
|
679
|
+
@prop rangeFrom = $bindable()
|
|
680
|
+
@prop rangeTo = $bindable()
|
|
681
|
+
@prop availableFrom = null
|
|
682
|
+
@prop availableTo = null
|
|
683
|
+
@prop locale = "default"
|
|
684
|
+
@prop translationLocale = locale
|
|
685
|
+
@prop firstDayOfWeek = 0
|
|
686
|
+
@prop dateFormat
|
|
687
|
+
@prop placeholder = "Select date"
|
|
688
|
+
@prop disabled = false
|
|
689
|
+
@prop required = false
|
|
690
|
+
@prop inputClass = ""
|
|
691
|
+
@prop color = "primary"
|
|
692
|
+
@prop inline = false
|
|
693
|
+
@prop autohide = true
|
|
694
|
+
@prop showActionButtons = false
|
|
695
|
+
@prop title = ""
|
|
696
|
+
@prop onselect
|
|
697
|
+
@prop onclear
|
|
698
|
+
@prop onapply
|
|
699
|
+
@prop btnClass
|
|
700
|
+
@prop inputmode = "none"
|
|
701
|
+
@prop classes
|
|
702
|
+
@prop monthColor = "alternative"
|
|
703
|
+
@prop monthBtnSelected = "bg-primary-500 text-white"
|
|
704
|
+
@prop monthBtn = "text-gray-700 dark:text-gray-300"
|
|
705
|
+
@prop class: className
|
|
706
|
+
@prop elementRef = $bindable()
|
|
707
|
+
-->
|