design-system-next 2.11.9 → 2.11.15

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.
@@ -0,0 +1,957 @@
1
+ import { onClickOutside, useVModel } from '@vueuse/core';
2
+ import { computed, ComputedRef, nextTick, onMounted, ref, SetupContext, toRefs, watch, useSlots } from 'vue';
3
+
4
+ import dayjs from 'dayjs';
5
+ import isBetween from 'dayjs/plugin/isBetween';
6
+ import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
7
+ import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
8
+
9
+ import classNames from 'classnames';
10
+
11
+ import type { DateRangePickerEmitTypes, DateRangePickerPropTypes } from './date-range-picker';
12
+ import { PLACEMENTS_TYPES } from './date-range-picker';
13
+
14
+ dayjs.extend(isSameOrBefore);
15
+ dayjs.extend(isSameOrAfter);
16
+ dayjs.extend(isBetween);
17
+
18
+ interface DateRangePickerClasses {
19
+ labelClasses: string;
20
+ dateRangePickerBaseInputClasses: string;
21
+ dateRangePickerInputClasses: string;
22
+ dateRangePickerInputHelperClasses: string;
23
+ calendarTabItemsBaseClasses: string;
24
+ monthsTabItemsBaseClasses: string;
25
+ yearsTabItemsBaseClasses: string;
26
+ }
27
+
28
+ interface MonthsList {
29
+ text: string;
30
+ fullText: string;
31
+ monthValue: number;
32
+ }
33
+
34
+ type DayAbbreviation = 'su' | 'mo' | 'tu' | 'we' | 'th' | 'fr' | 'sa';
35
+
36
+ export const useDateRangePicker = (props: DateRangePickerPropTypes, emit: SetupContext<DateRangePickerEmitTypes>['emit']) => {
37
+ const { active, disabled, readonly, error, currentYear, minMaxYear, restDays, disabledDates, format } = toRefs(props);
38
+ const slots = useSlots();
39
+
40
+ const modelValue = useVModel(props, 'modelValue', emit);
41
+
42
+ const dateRangePickerClasses: ComputedRef<DateRangePickerClasses> = computed(() => {
43
+ const labelClasses = classNames('spr-body-sm-regular spr-text-color-strong spr-block', {
44
+ 'spr-text-color-on-fill-disabled': disabled.value,
45
+ });
46
+
47
+ const dateRangePickerBaseInputClasses = classNames(
48
+ 'spr-flex spr-justify-between spr-items-center spr-gap-6 spr-rounded-lg spr-bg-white-50 spr-min-w-[180px] spr-py-1.5 spr-px-3',
49
+ {
50
+ // Normal State
51
+ 'spr-border spr-border-solid spr-border-mushroom-200 focus:spr-border-kangkong-700':
52
+ (!error.value && !disabled.value && !active.value && !datePopperState.value) || readonly.value,
53
+
54
+ // Active State
55
+ 'spr-border spr-border-solid spr-border-kangkong-700 spr-border-[1.5px] spr-border-solid':
56
+ (active.value || datePopperState.value === true) && !readonly.value,
57
+
58
+ // Error State
59
+ 'spr-border spr-border-solid spr-border-tomato-600 focus:spr-border-tomato-600': error.value,
60
+
61
+ // Disabled State
62
+ 'spr-background-color-disabled spr-border-white-100 focus:spr-border-white-100 spr-cursor-not-allowed !spr-text-white-400':
63
+ disabled.value,
64
+
65
+ // Readonly State
66
+ 'spr-cursor-default': readonly.value,
67
+ },
68
+ );
69
+
70
+ const dateRangePickerInputClasses = classNames(
71
+ 'spr-h-full spr-border-none spr-bg-transparent spr-outline-none',
72
+ 'spr-font-size-200',
73
+ 'placeholder:spr-text-color-weak',
74
+ {
75
+ 'spr-text-color-strong': !disabled.value && !readonly.value,
76
+ 'spr-text-color-on-fill-disabled': disabled.value || readonly.value,
77
+ 'spr-cursor-not-allowed': disabled.value,
78
+ },
79
+ );
80
+
81
+ const dateRangePickerInputHelperClasses = classNames(
82
+ 'spr-body-sm-regular',
83
+ 'spr-flex spr-items-center spr-gap-size-spacing-5xs',
84
+ 'spr-mt-size-spacing-4xs',
85
+ {
86
+ 'spr-text-color-danger-base': error.value,
87
+ 'spr-text-color-supporting': !error.value,
88
+ },
89
+ );
90
+
91
+ const calendarTabItemsBaseClasses = classNames(
92
+ 'spr-relative spr-box-border spr-flex spr-h-[40px] spr-items-center spr-justify-center spr-p-2',
93
+ 'spr-transition spr-duration-150 spr-ease-in-out',
94
+ );
95
+
96
+ const monthsTabItemsBaseClasses = classNames(
97
+ 'spr-subheading-xs spr-relative spr-flex spr-cursor-pointer spr-items-center spr-justify-center spr-rounded-lg spr-p-4',
98
+ 'spr-border spr-border-solid',
99
+ 'spr-transition spr-duration-150 spr-ease-in-out',
100
+ 'active:spr-scale-95',
101
+ );
102
+
103
+ const yearsTabItemsBaseClasses = classNames(
104
+ 'spr-subheading-xs spr-relative spr-flex spr-cursor-pointer spr-items-center spr-justify-center spr-rounded-lg spr-p-4',
105
+ 'spr-border spr-border-solid',
106
+ 'spr-transition spr-duration-150 spr-ease-in-out',
107
+ 'active:spr-scale-95',
108
+ );
109
+
110
+ return {
111
+ labelClasses,
112
+ dateRangePickerBaseInputClasses,
113
+ dateRangePickerInputClasses,
114
+ dateRangePickerInputHelperClasses,
115
+ calendarTabItemsBaseClasses,
116
+ monthsTabItemsBaseClasses,
117
+ yearsTabItemsBaseClasses,
118
+ };
119
+ });
120
+
121
+ const dateRangePickerRef = ref<HTMLInputElement | null>(null);
122
+ const startMonthInputRef = ref<HTMLInputElement | null>(null);
123
+ const startDateInputRef = ref<HTMLInputElement | null>(null);
124
+ const startYearInputRef = ref<HTMLInputElement | null>(null);
125
+ const endMonthInputRef = ref<HTMLInputElement | null>(null);
126
+ const endDateInputRef = ref<HTMLInputElement | null>(null);
127
+ const endYearInputRef = ref<HTMLInputElement | null>(null);
128
+
129
+ // Refs for container elements
130
+ const startDateContainerRef = ref<HTMLElement | null>(null);
131
+ const endDateContainerRef = ref<HTMLElement | null>(null);
132
+
133
+ // Track which input was clicked
134
+ const activeInputRef = ref<HTMLElement | null>(null);
135
+ const clickedInputType = ref<'start' | 'end'>('start');
136
+
137
+ const datePopperState = ref<boolean>(false);
138
+ const currentTab = ref<string>('tab-calendar');
139
+ const currentDate = ref(dayjs());
140
+ const selectionMode = ref<'start' | 'end'>('start');
141
+
142
+ const daysOfWeek = ref(
143
+ Array.from({ length: 7 }, (_, i) => ({
144
+ text: dayjs().day(i).format('dd'),
145
+ fullText: dayjs().day(i).format('dddd'),
146
+ dayValue: i,
147
+ })),
148
+ );
149
+
150
+ const monthsList = ref<MonthsList[]>(
151
+ Array.from({ length: 12 }, (_, i) => ({
152
+ text: dayjs().month(i).format('MMM'),
153
+ fullText: dayjs().month(i).format('MMMM'),
154
+ monthValue: i,
155
+ }))
156
+ );
157
+
158
+ // Start date inputs
159
+ const startMonthInput = ref<string>('');
160
+ const startDateInput = ref<string>('');
161
+ const startYearInput = ref<string>('');
162
+
163
+ // End date inputs
164
+ const endMonthInput = ref<string>('');
165
+ const endDateInput = ref<string>('');
166
+ const endYearInput = ref<string>('');
167
+
168
+ const dateRangePickerErrors = ref<{ title: string; message: string }[]>([]);
169
+
170
+ // #region - Calendar Tab
171
+ const calendarTabPageData = ref({
172
+ selectedMonth: dayjs().month(),
173
+ selectedYear: Number(currentYear.value),
174
+ calendarDays: [] as Array<{ date: dayjs.Dayjs; inactive: boolean }>,
175
+ });
176
+
177
+ const calendarTabIsMinMonth = computed(() =>
178
+ dayjs()
179
+ .year(calendarTabPageData.value.selectedYear)
180
+ .month(calendarTabPageData.value.selectedMonth)
181
+ .isSame(dayjs().year(minMaxYear.value.min).month(0), 'month'),
182
+ );
183
+
184
+ const calendarTabIsMaxMonth = computed(() =>
185
+ dayjs()
186
+ .year(calendarTabPageData.value.selectedYear)
187
+ .month(calendarTabPageData.value.selectedMonth)
188
+ .isSame(dayjs().year(minMaxYear.value.max).month(11), 'month'),
189
+ );
190
+
191
+ const calendarTabUpdateCalendar = () => {
192
+ const firstDay = dayjs()
193
+ .year(calendarTabPageData.value.selectedYear)
194
+ .month(calendarTabPageData.value.selectedMonth)
195
+ .startOf('month')
196
+ .day();
197
+
198
+ const lastDate = dayjs()
199
+ .year(calendarTabPageData.value.selectedYear)
200
+ .month(calendarTabPageData.value.selectedMonth)
201
+ .endOf('month')
202
+ .date();
203
+
204
+ const prevMonthDays = firstDay;
205
+ const totalDays = prevMonthDays + lastDate;
206
+ const nextMonthDays = totalDays % 7 === 0 ? 0 : 7 - (totalDays % 7);
207
+
208
+ const calendar = [];
209
+
210
+ // Previous month days
211
+ for (let index = prevMonthDays; index > 0; index--) {
212
+ const date = dayjs()
213
+ .year(calendarTabPageData.value.selectedYear)
214
+ .month(calendarTabPageData.value.selectedMonth)
215
+ .date(-index + 1);
216
+
217
+ calendar.push({ date, inactive: true });
218
+ }
219
+
220
+ // Current month days
221
+ for (let index = 1; index <= lastDate; index++) {
222
+ const date = dayjs()
223
+ .year(calendarTabPageData.value.selectedYear)
224
+ .month(calendarTabPageData.value.selectedMonth)
225
+ .date(index);
226
+
227
+ calendar.push({ date, inactive: false });
228
+ }
229
+
230
+ // Next month days
231
+ for (let index = 1; index <= nextMonthDays; index++) {
232
+ const date = dayjs()
233
+ .year(calendarTabPageData.value.selectedYear)
234
+ .month(calendarTabPageData.value.selectedMonth)
235
+ .date(lastDate + index);
236
+
237
+ calendar.push({ date, inactive: true });
238
+ }
239
+
240
+ calendarTabPageData.value.calendarDays = calendar;
241
+ };
242
+
243
+ const calendarTabPrevMonth = () => {
244
+ if (calendarTabPageData.value.selectedMonth === 0) {
245
+ calendarTabPageData.value.selectedMonth = 11;
246
+ calendarTabPageData.value.selectedYear--;
247
+ } else {
248
+ calendarTabPageData.value.selectedMonth--;
249
+ }
250
+ calendarTabUpdateCalendar();
251
+ };
252
+
253
+ const calendarTabNextMonth = () => {
254
+ if (calendarTabPageData.value.selectedMonth === 11) {
255
+ calendarTabPageData.value.selectedMonth = 0;
256
+ calendarTabPageData.value.selectedYear++;
257
+ } else {
258
+ calendarTabPageData.value.selectedMonth++;
259
+ }
260
+ calendarTabUpdateCalendar();
261
+ };
262
+
263
+ const calendarTabIsRestDay = (day: { date: dayjs.Dayjs; inactive: boolean }) => {
264
+ if (restDays.value.length === 0) return false;
265
+ // Use 'dd' format returns e.g. 'Su', 'Mo', etc. We need to map to 'su', 'mo', etc.
266
+ const dayOfWeek = day.date.format('dd').toLowerCase() as DayAbbreviation;
267
+ return restDays.value.includes(dayOfWeek);
268
+ };
269
+
270
+ const calendarTabIsTodayIndicator = (day: { date: dayjs.Dayjs }) => {
271
+ return day.date.isSame(dayjs(), 'day');
272
+ };
273
+
274
+ const calendarTabIsActiveMonthDates = (day: { date: dayjs.Dayjs; inactive: boolean }) => {
275
+ return !day.inactive;
276
+ };
277
+
278
+ const calendarTabIsInactiveMonthDates = (day: { date: dayjs.Dayjs; inactive: boolean }) => {
279
+ return day.inactive;
280
+ };
281
+
282
+ const calendarTabIsSelectedDate = (day: { date: dayjs.Dayjs; inactive: boolean }) => {
283
+
284
+ const startDate = dayjs(modelValue.value.startDate).format('MM-DD-YYYY');
285
+ const endDate = dayjs(modelValue.value.endDate).format('MM-DD-YYYY');
286
+
287
+ const dayDate = day.date.format('MM-DD-YYYY');
288
+ // If only start date is selected, highlight it
289
+ if (startDate && !endDate) {
290
+ return dayDate === startDate;
291
+ }
292
+
293
+ // If both dates are selected, highlight start and end
294
+ if (startDate && endDate) {
295
+ return dayDate === startDate || dayDate === endDate;
296
+ }
297
+
298
+ return false;
299
+ };
300
+
301
+ const calendarTabIsInRange = (day: { date: dayjs.Dayjs; inactive: boolean }) => {
302
+ const startDate = modelValue.value.startDate;
303
+ const endDate = modelValue.value.endDate;
304
+
305
+ // Only show range when both dates are selected
306
+ if (!startDate || !endDate) return false;
307
+
308
+ const dayDate = day.date;
309
+ const start = dayjs(startDate, 'MM-DD-YYYY');
310
+ const end = dayjs(endDate, 'MM-DD-YYYY');
311
+
312
+ return dayDate.isAfter(start, 'day') && dayDate.isBefore(end, 'day');
313
+ };
314
+
315
+ const calendarTabIsUnSelectedDate = (day: { date: dayjs.Dayjs }) => {
316
+ const startDate = modelValue.value.startDate;
317
+ const endDate = modelValue.value.endDate;
318
+
319
+ if (!startDate || !endDate) return true;
320
+
321
+ const dayDate = day.date.format('MM-DD-YYYY');
322
+ return dayDate !== startDate && dayDate !== endDate;
323
+ };
324
+
325
+ const calendarTabIsDateIsDisabled = (day: { date: dayjs.Dayjs }) => {
326
+ return (
327
+ calendarTabIsDateDisabledFromTo(day) ||
328
+ calendarTabIsDateDisabledPastDate(day) ||
329
+ calendarTabIsDateDisabledFutureDate(day) ||
330
+ calendarTabIsDateDisabledSelectedDates(day) ||
331
+ calendarTabIsDateDisabledWeekends(day) ||
332
+ calendarTabIsDateDisabledWeekdays(day) ||
333
+ calendarTabIsDateDisabledSelectedDays(day)
334
+ );
335
+ };
336
+
337
+ const calendarTabIsDateDisabledFromTo = (day: { date: dayjs.Dayjs }) => {
338
+ if (!safeDisabledDates.value?.from || !safeDisabledDates.value?.to) return false;
339
+ const dayDate = day.date;
340
+ const fromDate = dayjs(safeDisabledDates.value.from, 'MM-DD-YYYY');
341
+ const toDate = dayjs(safeDisabledDates.value.to, 'MM-DD-YYYY');
342
+ return dayDate.isBetween(fromDate, toDate, 'day', '[]');
343
+ };
344
+
345
+ const calendarTabIsDateDisabledPastDate = (day: { date: dayjs.Dayjs }) => {
346
+ if (!safeDisabledDates.value?.pastDates) return false;
347
+ const dayDate = day.date;
348
+ const currentDate = dayjs();
349
+
350
+ if (typeof safeDisabledDates.value.pastDates === 'string') {
351
+ const pastDate = dayjs(safeDisabledDates.value.pastDates, 'MM-DD-YYYY');
352
+ return dayDate.isBefore(pastDate, 'day');
353
+ }
354
+
355
+ return dayDate.isBefore(currentDate, 'day');
356
+ };
357
+
358
+ const calendarTabIsDateDisabledFutureDate = (day: { date: dayjs.Dayjs }) => {
359
+ if (!safeDisabledDates.value?.futureDates) return false;
360
+ const dayDate = day.date;
361
+ const currentDate = dayjs();
362
+
363
+ if (typeof safeDisabledDates.value.futureDates === 'string') {
364
+ const futureDate = dayjs(safeDisabledDates.value.futureDates, 'MM-DD-YYYY');
365
+ return dayDate.isAfter(futureDate, 'day');
366
+ }
367
+
368
+ return dayDate.isAfter(currentDate, 'day');
369
+ };
370
+
371
+ const calendarTabIsDateDisabledSelectedDates = (day: { date: dayjs.Dayjs }) => {
372
+ if (!safeDisabledDates.value?.selectedDates) return false;
373
+ const dayDate = day.date.format('MM-DD-YYYY');
374
+ return safeDisabledDates.value.selectedDates.includes(dayDate);
375
+ };
376
+
377
+ const calendarTabIsDateDisabledWeekends = (day: { date: dayjs.Dayjs }) => {
378
+ if (!safeDisabledDates.value?.weekends) return false;
379
+ const dayOfWeek = day.date.day();
380
+ return dayOfWeek === 0 || dayOfWeek === 6;
381
+ };
382
+
383
+ const calendarTabIsDateDisabledWeekdays = (day: { date: dayjs.Dayjs }) => {
384
+ if (!safeDisabledDates.value?.weekdays) return false;
385
+ const dayOfWeek = day.date.day();
386
+ return dayOfWeek >= 1 && dayOfWeek <= 5;
387
+ };
388
+
389
+ const calendarTabIsDateDisabledSelectedDays = (day: { date: dayjs.Dayjs }) => {
390
+ if (!safeDisabledDates.value?.selectedDays) return false;
391
+ const dayOfWeek = day.date.format('dd').toLowerCase() as DayAbbreviation;
392
+ return safeDisabledDates.value.selectedDays.includes(dayOfWeek);
393
+ };
394
+
395
+ // Update calendarTabHandleDateInput to use format.value
396
+ const calendarTabHandleDateInput = (day: { date: dayjs.Dayjs }) => {
397
+ if (calendarTabIsDateIsDisabled(day)) return;
398
+
399
+ const selectedDate = dayjs(day.date).format(format.value);
400
+
401
+ // If no start date is selected, set it as start date
402
+ if (!modelValue.value.startDate) {
403
+ modelValue.value = {
404
+ ...modelValue.value,
405
+ startDate: selectedDate,
406
+ };
407
+ selectionMode.value = 'end';
408
+ }
409
+ // If start date is selected but no end date, set it as end date
410
+ else if (!modelValue.value.endDate) {
411
+ // If selected date is before start date, swap them
412
+ if (dayjs(selectedDate, format.value).isBefore(dayjs(modelValue.value.startDate, format.value))) {
413
+ modelValue.value = {
414
+ startDate: selectedDate,
415
+ endDate: modelValue.value.startDate,
416
+ };
417
+ } else {
418
+ modelValue.value = {
419
+ ...modelValue.value,
420
+ endDate: selectedDate,
421
+ };
422
+ }
423
+ selectionMode.value = 'start';
424
+ datePopperState.value = false;
425
+ }
426
+ // If both dates are selected, start a new selection
427
+ else {
428
+ modelValue.value = {
429
+ startDate: selectedDate,
430
+ endDate: '',
431
+ };
432
+ selectionMode.value = 'end';
433
+ }
434
+
435
+ updateInputFields();
436
+ emitRangeChange();
437
+ };
438
+
439
+ // #region - Month Tab
440
+ const monthTabHandleSelectedMonth = (selectedMonth: { text: string; fullText: string; monthValue: number }) => {
441
+ calendarTabPageData.value.selectedMonth = selectedMonth.monthValue;
442
+ calendarTabUpdateCalendar();
443
+ currentTab.value = 'tab-calendar';
444
+ };
445
+
446
+ // #region - Year Tab
447
+ const yearsArray = Array.from(
448
+ { length: minMaxYear.value.max - minMaxYear.value.min + 1 },
449
+ (_, index) => minMaxYear.value.min + index,
450
+ ).filter((year) => year <= minMaxYear.value.max && year >= minMaxYear.value.min);
451
+
452
+ const yearTabPageData = ref({
453
+ currentPage: 0,
454
+ itemsPerPage: 12,
455
+ yearsArray,
456
+ });
457
+
458
+ const yearTabCurrentYearPage = computed(() => {
459
+ const start = yearTabPageData.value.currentPage * yearTabPageData.value.itemsPerPage;
460
+ const remainingItems = yearTabPageData.value.yearsArray.slice(start);
461
+ return remainingItems.length < yearTabPageData.value.itemsPerPage
462
+ ? remainingItems
463
+ : yearTabPageData.value.yearsArray.slice(start, start + yearTabPageData.value.itemsPerPage);
464
+ });
465
+
466
+ const yearsTabSetCurrentPageYear = () => {
467
+ const currentYearIndex = yearTabPageData.value.yearsArray.indexOf(calendarTabPageData.value.selectedYear);
468
+ if (currentYearIndex !== -1) {
469
+ yearTabPageData.value.currentPage = Math.floor(currentYearIndex / yearTabPageData.value.itemsPerPage);
470
+ }
471
+ };
472
+
473
+ const yearTabGoToPreviousPage = () => {
474
+ if (yearTabPageData.value.currentPage > 0) {
475
+ yearTabPageData.value.currentPage--;
476
+ }
477
+ };
478
+
479
+ const yearTabGoToNextPage = () => {
480
+ if ((yearTabPageData.value.currentPage + 1) * yearTabPageData.value.itemsPerPage < yearTabPageData.value.yearsArray.length) {
481
+ yearTabPageData.value.currentPage++;
482
+ }
483
+ };
484
+
485
+ const yearTabIsPreviousButtonDisabled = computed(() => {
486
+ return yearTabPageData.value.currentPage === 0;
487
+ });
488
+
489
+ const yearTabIsNextButtonDisabled = computed(() => {
490
+ return (
491
+ (yearTabPageData.value.currentPage + 1) * yearTabPageData.value.itemsPerPage >= yearTabPageData.value.yearsArray.length
492
+ );
493
+ });
494
+
495
+ const yearTabHandleSelectedYear = (selectedYear: string) => {
496
+ calendarTabPageData.value.selectedYear = Number(selectedYear);
497
+ // Fill the correct year input based on selectionMode
498
+ if (selectionMode.value === 'start') {
499
+ startYearInput.value = selectedYear;
500
+ } else {
501
+ endYearInput.value = selectedYear;
502
+ }
503
+ calendarTabUpdateCalendar();
504
+ currentTab.value = 'tab-calendar';
505
+ yearsTabSetCurrentPageYear();
506
+ };
507
+
508
+ // #region - Input Handling
509
+ // Update setWarningPropsValue to use format.value for error messages
510
+ const setWarningPropsValue = (type: 'startDate' | 'endDate' | 'range' | 'year') => {
511
+ const warningProps = {
512
+ startDate: {
513
+ title: 'Invalid Start Date',
514
+ message: `Please enter a valid start date in the format ${format.value}`,
515
+ },
516
+ endDate: {
517
+ title: 'Invalid End Date',
518
+ message: `Please enter a valid end date in the format ${format.value}`,
519
+ },
520
+ range: {
521
+ title: 'Invalid Date Range',
522
+ message: 'End date must be after start date',
523
+ },
524
+ year: {
525
+ title: 'Invalid Year',
526
+ message: 'Year must be between the minimum and maximum allowed years',
527
+ },
528
+ };
529
+
530
+ const existingIndex = dateRangePickerErrors.value.findIndex((error) => error.title === warningProps[type].title);
531
+ if (existingIndex === -1) {
532
+ dateRangePickerErrors.value.push(warningProps[type]);
533
+ }
534
+ };
535
+
536
+ const clearWarningPropsValue = (type: 'startDate' | 'endDate' | 'range' | 'year') => {
537
+ const warningProps = {
538
+ startDate: 'Invalid Start Date',
539
+ endDate: 'Invalid End Date',
540
+ range: 'Invalid Date Range',
541
+ year: 'Invalid Year',
542
+ };
543
+
544
+ const index = dateRangePickerErrors.value.findIndex((error) => error.title === warningProps[type]);
545
+ if (index !== -1) {
546
+ dateRangePickerErrors.value.splice(index, 1);
547
+ }
548
+ };
549
+
550
+ // Update setModelValue to use format.value for parsing and formatting
551
+ const setModelValue = () => {
552
+ const startDateValid = validateDate(startMonthInput.value, startDateInput.value, startYearInput.value);
553
+ const endDateValid = validateDate(endMonthInput.value, endDateInput.value, endYearInput.value);
554
+
555
+ if (startDateValid && endDateValid) {
556
+ const startDate = `${startMonthInput.value}-${startDateInput.value}-${startYearInput.value}`;
557
+ const endDate = `${endMonthInput.value}-${endDateInput.value}-${endYearInput.value}`;
558
+ // Validate range
559
+ if (dayjs(startDate, format.value).isAfter(dayjs(endDate, format.value))) {
560
+ setWarningPropsValue('range');
561
+ return;
562
+ } else {
563
+ clearWarningPropsValue('range');
564
+ }
565
+ modelValue.value = {
566
+ startDate: dayjs(startDate, 'MMM-DD-YYYY').format(format.value),
567
+ endDate: dayjs(endDate, 'MMM-DD-YYYY').format(format.value),
568
+ };
569
+ emitRangeChange();
570
+ }
571
+ };
572
+
573
+ const validateDate = (month: string, date: string, year: string): boolean => {
574
+ if (!month || !date || !year) return false;
575
+
576
+ const monthObj = getMonthObject('monthValue', month);
577
+ if (!monthObj) {
578
+ setWarningPropsValue('startDate');
579
+ return false;
580
+ }
581
+
582
+ const dayjsDate = dayjs(`${monthObj.monthValue + 1}-${date}-${year}`, 'M-D-YYYY');
583
+ if (!dayjsDate.isValid()) {
584
+ setWarningPropsValue('startDate');
585
+ return false;
586
+ }
587
+
588
+ const yearNum = Number(year);
589
+ if (yearNum < minMaxYear.value.min || yearNum > minMaxYear.value.max) {
590
+ setWarningPropsValue('year');
591
+ return false;
592
+ }
593
+
594
+ clearWarningPropsValue('startDate');
595
+ clearWarningPropsValue('year');
596
+ return true;
597
+ };
598
+
599
+ const getMonthObject = (field: 'text' | 'fullText' | 'monthValue', value: string | number) => {
600
+ return monthsList.value.find((month) => month[field] === value);
601
+ };
602
+
603
+ const getDatePickerInputClasses = (width: string) => {
604
+ return `spr-w-[${width}] spr-min-w-[${width}]`;
605
+ };
606
+
607
+ const getTabClasses = (tab: string) => {
608
+ return classNames('spr-cursor-pointer', {
609
+ 'spr-background-color-pressed !spr-shadow-button': currentTab.value === tab,
610
+ });
611
+ };
612
+
613
+ const handleStartMonthInput = () => {
614
+ startMonthInput.value = startMonthInput.value.replace(/[^A-Za-z0-9\s]/g, '').toLocaleUpperCase();
615
+ handleConvertMonthIfValid('start');
616
+ setModelValue();
617
+ };
618
+
619
+ const handleStartDateInput = () => {
620
+ if (startDateInput.value.length === 2) {
621
+ const day = Number(startDateInput.value);
622
+ if (day < 1 || day > 31) {
623
+ startDateInput.value = '';
624
+ return;
625
+ }
626
+ }
627
+ setModelValue();
628
+ };
629
+
630
+ const handleStartYearInput = () => {
631
+ if (startYearInput.value.length === 4) {
632
+ const year = Number(startYearInput.value);
633
+ if (year < minMaxYear.value.min || year > minMaxYear.value.max) {
634
+ startYearInput.value = '';
635
+ return;
636
+ }
637
+ }
638
+ setModelValue();
639
+ };
640
+
641
+ const handleEndMonthInput = () => {
642
+ endMonthInput.value = endMonthInput.value.replace(/[^A-Za-z0-9\s]/g, '').toLocaleUpperCase();
643
+ handleConvertMonthIfValid('end');
644
+ setModelValue();
645
+ };
646
+
647
+ const handleEndDateInput = () => {
648
+ if (endDateInput.value.length === 2) {
649
+ const day = Number(endDateInput.value);
650
+ if (day < 1 || day > 31) {
651
+ endDateInput.value = '';
652
+ return;
653
+ }
654
+ }
655
+ setModelValue();
656
+ };
657
+
658
+ const handleEndYearInput = () => {
659
+ if (endYearInput.value.length === 4) {
660
+ const year = Number(endYearInput.value);
661
+ if (year < minMaxYear.value.min || year > minMaxYear.value.max) {
662
+ endYearInput.value = '';
663
+ return;
664
+ }
665
+ }
666
+ setModelValue();
667
+ };
668
+
669
+ const handleConvertMonthIfValid = (type: 'start' | 'end') => {
670
+ const monthInput = type === 'start' ? startMonthInput.value : endMonthInput.value;
671
+ const monthObj = monthsList.value.find(
672
+ (month) => month.text.toLowerCase() === monthInput.toLowerCase(),
673
+ );
674
+
675
+ if (monthObj) {
676
+ if (type === 'start') {
677
+ startMonthInput.value = monthObj.text;
678
+ } else {
679
+ endMonthInput.value = monthObj.text;
680
+ }
681
+ }
682
+ };
683
+
684
+ const handleTabClick = (tab: string) => {
685
+ currentTab.value = tab;
686
+ };
687
+
688
+ const handleBackspace = (inputType: string, event: KeyboardEvent) => {
689
+ if (event && event instanceof KeyboardEvent && event.key === 'Backspace') {
690
+ // Start date fields
691
+ if (inputType === 'start-date' && startDateInput.value === '') {
692
+ nextTick(() => {
693
+ if (startMonthInputRef.value) startMonthInputRef.value.focus();
694
+ });
695
+ }
696
+ if (inputType === 'start-year' && startYearInput.value === '') {
697
+ nextTick(() => {
698
+ if (startDateInputRef.value) startDateInputRef.value.focus();
699
+ });
700
+ }
701
+ // End date fields
702
+ if (inputType === 'end-date' && endDateInput.value === '') {
703
+ nextTick(() => {
704
+ if (endMonthInputRef.value) endMonthInputRef.value.focus();
705
+ });
706
+ }
707
+ if (inputType === 'end-year' && endYearInput.value === '') {
708
+ nextTick(() => {
709
+ if (endDateInputRef.value) endDateInputRef.value.focus();
710
+ });
711
+ }
712
+ }
713
+ };
714
+
715
+ const updateInputFields = () => {
716
+ if (modelValue.value.startDate) {
717
+ const startDate = dayjs(modelValue.value.startDate, 'MM-DD-YYYY');
718
+ startMonthInput.value = startDate.format('MMM');
719
+ startDateInput.value = startDate.format('DD');
720
+ startYearInput.value = startDate.format('YYYY');
721
+ }
722
+
723
+ if (modelValue.value.endDate) {
724
+ const endDate = dayjs(modelValue.value.endDate, 'MM-DD-YYYY');
725
+ endMonthInput.value = endDate.format('MMM');
726
+ endDateInput.value = endDate.format('DD');
727
+ endYearInput.value = endDate.format('YYYY');
728
+ }
729
+ };
730
+
731
+ const emitRangeChange = () => {
732
+ const isValid = !dateRangePickerErrors.value.length;
733
+ emit('rangeChange', {
734
+ startDate: modelValue.value.startDate,
735
+ endDate: modelValue.value.endDate,
736
+ isValid,
737
+ });
738
+ };
739
+
740
+ const emitDateFormats = () => {
741
+ if (modelValue.value.startDate && modelValue.value.endDate) {
742
+ const startDate = dayjs(modelValue.value.startDate, 'MM-DD-YYYY');
743
+ const endDate = dayjs(modelValue.value.endDate, 'MM-DD-YYYY');
744
+
745
+ const formats = {
746
+ 'MM-DD-YYYY': {
747
+ startDate: startDate.format('MM-DD-YYYY'),
748
+ endDate: endDate.format('MM-DD-YYYY'),
749
+ },
750
+ 'YYYY-MM-DD': {
751
+ startDate: startDate.format('YYYY-MM-DD'),
752
+ endDate: endDate.format('YYYY-MM-DD'),
753
+ },
754
+ 'MM/DD/YYYY': {
755
+ startDate: startDate.format('MM/DD/YYYY'),
756
+ endDate: endDate.format('MM/DD/YYYY'),
757
+ },
758
+ 'DD-MM-YYYY': {
759
+ startDate: startDate.format('DD-MM-YYYY'),
760
+ endDate: endDate.format('DD-MM-YYYY'),
761
+ },
762
+ };
763
+
764
+ emit('getDateFormats', formats);
765
+ }
766
+ };
767
+
768
+ // Update emitInputValue to use format.value
769
+ const emitInputValue = () => {
770
+ emit('getInputValue', modelValue.value);
771
+ };
772
+
773
+ const emitMonthList = () => {
774
+ emit('getMonthList', monthsList.value);
775
+ };
776
+
777
+ const emitYearList = () => {
778
+ const years = Array.from(
779
+ { length: minMaxYear.value.max - minMaxYear.value.min + 1 },
780
+ (_, i) => minMaxYear.value.min + i,
781
+ );
782
+ emit('getYearList', years);
783
+ };
784
+
785
+ const emitDateErrors = () => {
786
+ emit('getDateErrors', dateRangePickerErrors.value);
787
+ };
788
+
789
+ // #region - Watchers
790
+ watch(modelValue, () => {
791
+ updateInputFields();
792
+ emitDateFormats();
793
+ emitInputValue();
794
+ emitDateErrors();
795
+ }, { deep: true });
796
+
797
+ watch(dateRangePickerErrors, () => {
798
+ emitDateErrors();
799
+ }, { deep: true });
800
+
801
+ // #region - Lifecycle
802
+ onMounted(() => {
803
+ calendarTabUpdateCalendar();
804
+ yearsTabSetCurrentPageYear();
805
+ updateInputFields();
806
+ emitMonthList();
807
+ emitYearList();
808
+ });
809
+
810
+ const isDateRangePickerPopperDisabled = computed(() => {
811
+ return disabled.value || readonly.value;
812
+ });
813
+
814
+ // Provide a default for disabledDates to avoid undefined errors
815
+ const defaultDisabledDates = {
816
+ from: '',
817
+ to: '',
818
+ pastDates: false,
819
+ futureDates: false,
820
+ selectedDates: [],
821
+ weekends: false,
822
+ weekdays: false,
823
+ selectedDays: [],
824
+ };
825
+ const safeDisabledDates = computed(() => disabledDates?.value ?? defaultDisabledDates);
826
+
827
+ onClickOutside(dateRangePickerRef, () => {
828
+ datePopperState.value = false;
829
+ });
830
+
831
+ // #region - Positioning and Click Handling
832
+ // Check if user is using custom slot (not the default fallback)
833
+ const isUsingCustomSlot = computed(() => {
834
+ return !!slots.default;
835
+ });
836
+
837
+ // Dynamic placement based on which input was clicked (only for default inputs)
838
+ const dynamicPlacement = computed(() => {
839
+ // Get the base placement from props (top or bottom)
840
+ let basePlacement = props.placement;
841
+
842
+ if (basePlacement !== 'top' && basePlacement !== 'bottom') {
843
+ basePlacement = 'bottom';
844
+ }
845
+
846
+ if (clickedInputType.value === 'start') {
847
+ // For start date: concatenate base placement with 'start'
848
+ return `${basePlacement}-start`;
849
+ } else {
850
+ // For end date: concatenate base placement with 'end'
851
+ return `${basePlacement}-end`;
852
+ }
853
+ });
854
+
855
+ // Final placement: use dynamic for default inputs, standard for custom slots
856
+ const finalPlacement = computed(() => {
857
+ if (isUsingCustomSlot.value) {
858
+ return props.placement; // Use standard placement from props
859
+ } else {
860
+ return dynamicPlacement.value as (typeof PLACEMENTS_TYPES)[number]; // Use dynamic placement for default inputs
861
+ }
862
+ });
863
+
864
+ // Handle start date input clicks
865
+ const handleStartDateClick = () => {
866
+ if (disabled.value || readonly.value) return;
867
+
868
+ clickedInputType.value = 'start';
869
+ activeInputRef.value = startDateContainerRef.value;
870
+ datePopperState.value = true;
871
+ };
872
+
873
+ // Handle end date input clicks
874
+ const handleEndDateClick = () => {
875
+ if (disabled.value || readonly.value) return;
876
+
877
+ clickedInputType.value = 'end';
878
+ activeInputRef.value = endDateContainerRef.value;
879
+ datePopperState.value = true;
880
+ };
881
+
882
+ // Handle custom component clicks (for slot usage)
883
+ const handleCustomComponentClick = (event: Event) => {
884
+ if (disabled.value || readonly.value) return;
885
+
886
+ // For custom slots, use the clicked element as reference
887
+ activeInputRef.value = event.currentTarget as HTMLElement;
888
+ datePopperState.value = true;
889
+ };
890
+
891
+ return {
892
+ dateRangePickerClasses,
893
+ dateRangePickerRef,
894
+ startMonthInputRef,
895
+ startDateInputRef,
896
+ startYearInputRef,
897
+ endMonthInputRef,
898
+ endDateInputRef,
899
+ endYearInputRef,
900
+ startDateContainerRef,
901
+ endDateContainerRef,
902
+ activeInputRef,
903
+ datePopperState,
904
+ currentTab,
905
+ currentDate,
906
+ daysOfWeek,
907
+ monthsList,
908
+ startMonthInput,
909
+ startDateInput,
910
+ startYearInput,
911
+ endMonthInput,
912
+ endDateInput,
913
+ endYearInput,
914
+ dateRangePickerErrors,
915
+ calendarTabPageData,
916
+ calendarTabIsMinMonth,
917
+ calendarTabIsMaxMonth,
918
+ calendarTabUpdateCalendar,
919
+ calendarTabPrevMonth,
920
+ calendarTabNextMonth,
921
+ calendarTabIsRestDay,
922
+ calendarTabIsTodayIndicator,
923
+ calendarTabIsActiveMonthDates,
924
+ calendarTabIsInactiveMonthDates,
925
+ calendarTabIsSelectedDate,
926
+ calendarTabIsInRange,
927
+ calendarTabIsUnSelectedDate,
928
+ calendarTabIsDateIsDisabled,
929
+ calendarTabHandleDateInput,
930
+ monthTabHandleSelectedMonth,
931
+ yearTabCurrentYearPage,
932
+ yearTabGoToPreviousPage,
933
+ yearTabGoToNextPage,
934
+ yearTabIsPreviousButtonDisabled,
935
+ yearTabIsNextButtonDisabled,
936
+ yearTabHandleSelectedYear,
937
+ getMonthObject,
938
+ getDatePickerInputClasses,
939
+ getTabClasses,
940
+ isDateRangePickerPopperDisabled,
941
+ handleStartMonthInput,
942
+ handleStartDateInput,
943
+ handleStartYearInput,
944
+ handleEndMonthInput,
945
+ handleEndDateInput,
946
+ handleEndYearInput,
947
+ handleTabClick,
948
+ handleBackspace,
949
+ selectionMode,
950
+ isUsingCustomSlot,
951
+ dynamicPlacement,
952
+ finalPlacement,
953
+ handleStartDateClick,
954
+ handleEndDateClick,
955
+ handleCustomComponentClick,
956
+ };
957
+ };