feat: Replace vue-datepicker-next from codebase
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import {
|
||||
startOfMonth,
|
||||
addMonths,
|
||||
subMonths,
|
||||
startOfDay,
|
||||
isSameDay,
|
||||
addHours,
|
||||
addYears,
|
||||
subYears,
|
||||
setMonth,
|
||||
setYear,
|
||||
} from 'date-fns';
|
||||
import { CALENDAR_TYPES, CALENDAR_PERIODS } from './helpers/DatePickerHelper';
|
||||
import CalendarYear from './components/CalendarYear.vue';
|
||||
import CalendarMonth from './components/CalendarMonth.vue';
|
||||
import CalendarWeek from './components/CalendarWeek.vue';
|
||||
import CalendarFooter from './components/CalendarFooter.vue';
|
||||
import TimePicker from './components/TimePicker.vue';
|
||||
|
||||
defineProps({
|
||||
minDate: {
|
||||
type: Date,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['apply', 'clear']);
|
||||
|
||||
const { START_CALENDAR } = CALENDAR_TYPES;
|
||||
const { WEEK, MONTH, YEAR } = CALENDAR_PERIODS;
|
||||
|
||||
const currentDate = ref(new Date());
|
||||
const selectedDate = ref(null);
|
||||
|
||||
const getCurrentTime = () => {
|
||||
const now = new Date();
|
||||
return {
|
||||
hour: now.getHours(),
|
||||
minute: now.getMinutes(),
|
||||
second: now.getSeconds(),
|
||||
};
|
||||
};
|
||||
|
||||
const selectedTime = ref(getCurrentTime());
|
||||
const calendarView = ref(WEEK);
|
||||
const calendarDate = ref(startOfMonth(currentDate.value));
|
||||
|
||||
const getMinTimeForToday = () => addHours(new Date(), 1);
|
||||
|
||||
const minTime = computed(() => {
|
||||
if (!selectedDate.value) return null;
|
||||
if (!isSameDay(selectedDate.value, currentDate.value)) return null;
|
||||
return getMinTimeForToday();
|
||||
});
|
||||
|
||||
const selectedDateTime = computed(() => {
|
||||
if (!selectedDate.value) return null;
|
||||
const date = new Date(selectedDate.value);
|
||||
date.setHours(
|
||||
selectedTime.value.hour,
|
||||
selectedTime.value.minute,
|
||||
selectedTime.value.second,
|
||||
0
|
||||
);
|
||||
return date;
|
||||
});
|
||||
|
||||
const selectDate = day => {
|
||||
selectedDate.value = startOfDay(day);
|
||||
if (isSameDay(day, currentDate.value)) {
|
||||
const minDate = getMinTimeForToday();
|
||||
selectedTime.value = {
|
||||
hour: minDate.getHours(),
|
||||
minute: minDate.getMinutes(),
|
||||
second: 0,
|
||||
};
|
||||
} else {
|
||||
selectedTime.value = { hour: 0, minute: 0, second: 0 };
|
||||
}
|
||||
};
|
||||
|
||||
const moveCalendar = (direction, period = MONTH) => {
|
||||
const adjust =
|
||||
period === YEAR
|
||||
? { prev: subYears, next: addYears }
|
||||
: { prev: subMonths, next: addMonths };
|
||||
calendarDate.value = adjust[direction](calendarDate.value, 1);
|
||||
};
|
||||
|
||||
const setViewMode = (_calendar, mode) => {
|
||||
calendarView.value = mode;
|
||||
};
|
||||
|
||||
const openCalendar = (index, _calendarType, period = MONTH) => {
|
||||
calendarDate.value =
|
||||
period === MONTH
|
||||
? setMonth(startOfMonth(calendarDate.value), index)
|
||||
: setYear(calendarDate.value, index);
|
||||
calendarView.value = period === MONTH ? WEEK : MONTH;
|
||||
};
|
||||
|
||||
const onApply = () => {
|
||||
if (selectedDateTime.value) {
|
||||
emit('apply', selectedDateTime.value);
|
||||
}
|
||||
};
|
||||
|
||||
const isDefaultState = computed(
|
||||
() => !selectedDate.value && calendarView.value === WEEK
|
||||
);
|
||||
|
||||
const resetState = () => {
|
||||
selectedDate.value = null;
|
||||
selectedTime.value = getCurrentTime();
|
||||
calendarDate.value = startOfMonth(currentDate.value);
|
||||
calendarView.value = WEEK;
|
||||
};
|
||||
|
||||
const onClear = () => {
|
||||
if (isDefaultState.value) {
|
||||
emit('clear');
|
||||
return;
|
||||
}
|
||||
resetState();
|
||||
};
|
||||
|
||||
defineExpose({ resetState });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col select-none font-inter">
|
||||
<div class="flex w-full gap-3 justify-between">
|
||||
<div class="flex justify-center py-5">
|
||||
<div class="flex flex-col items-center gap-2 min-w-[300px]">
|
||||
<CalendarYear
|
||||
v-if="calendarView === YEAR"
|
||||
:calendar-type="START_CALENDAR"
|
||||
:start-current-date="calendarDate"
|
||||
:end-current-date="addMonths(calendarDate, 1)"
|
||||
@select-year="openCalendar($event, START_CALENDAR, YEAR)"
|
||||
/>
|
||||
<CalendarMonth
|
||||
v-else-if="calendarView === MONTH"
|
||||
:calendar-type="START_CALENDAR"
|
||||
:start-current-date="calendarDate"
|
||||
:end-current-date="addMonths(calendarDate, 1)"
|
||||
@select-month="openCalendar($event, START_CALENDAR)"
|
||||
@set-view="setViewMode"
|
||||
@prev="moveCalendar('prev', YEAR)"
|
||||
@next="moveCalendar('next', YEAR)"
|
||||
/>
|
||||
<CalendarWeek
|
||||
v-else
|
||||
:calendar-type="START_CALENDAR"
|
||||
:current-date="currentDate"
|
||||
:start-current-date="calendarDate"
|
||||
:end-current-date="addMonths(calendarDate, 1)"
|
||||
:selected-start-date="selectedDate"
|
||||
:selected-end-date="selectedDate"
|
||||
:selecting-end-date="false"
|
||||
:hovered-end-date="null"
|
||||
:min-date="minDate"
|
||||
@select-date="selectDate"
|
||||
@set-view="setViewMode"
|
||||
@prev="moveCalendar('prev')"
|
||||
@next="moveCalendar('next')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex justify-center py-2 w-full transition-opacity ltr:border-l rtl:border-r border-n-strong"
|
||||
:class="selectedDate ? 'opacity-100' : 'opacity-40 pointer-events-none'"
|
||||
>
|
||||
<TimePicker v-model="selectedTime" :min-time="minTime" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-n-strong">
|
||||
<CalendarFooter @change="onApply" @clear="onClear" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,455 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import {
|
||||
getActiveDateRange,
|
||||
moveCalendarDate,
|
||||
DATE_RANGE_TYPES,
|
||||
CALENDAR_TYPES,
|
||||
CALENDAR_PERIODS,
|
||||
isNavigableRange,
|
||||
getRangeAtOffset,
|
||||
} from './helpers/DatePickerHelper';
|
||||
import {
|
||||
isValid,
|
||||
startOfMonth,
|
||||
subDays,
|
||||
startOfDay,
|
||||
endOfDay,
|
||||
subMonths,
|
||||
addMonths,
|
||||
isSameMonth,
|
||||
differenceInCalendarMonths,
|
||||
differenceInCalendarWeeks,
|
||||
setMonth,
|
||||
setYear,
|
||||
getWeek,
|
||||
} from 'date-fns';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import DatePickerButton from './components/DatePickerButton.vue';
|
||||
import CalendarDateInput from './components/CalendarDateInput.vue';
|
||||
import CalendarDateRange from './components/CalendarDateRange.vue';
|
||||
import CalendarYear from './components/CalendarYear.vue';
|
||||
import CalendarMonth from './components/CalendarMonth.vue';
|
||||
import CalendarWeek from './components/CalendarWeek.vue';
|
||||
import CalendarFooter from './components/CalendarFooter.vue';
|
||||
|
||||
const emit = defineEmits(['dateRangeChanged']);
|
||||
const { t } = useI18n();
|
||||
|
||||
const dateRange = defineModel('dateRange', {
|
||||
type: Array,
|
||||
default: undefined,
|
||||
});
|
||||
|
||||
const rangeType = defineModel('rangeType', {
|
||||
type: String,
|
||||
default: undefined,
|
||||
});
|
||||
const { LAST_7_DAYS, CUSTOM_RANGE } = DATE_RANGE_TYPES;
|
||||
const { START_CALENDAR, END_CALENDAR } = CALENDAR_TYPES;
|
||||
const { WEEK, MONTH, YEAR } = CALENDAR_PERIODS;
|
||||
|
||||
const showDatePicker = ref(false);
|
||||
const calendarViews = ref({ start: WEEK, end: WEEK });
|
||||
const currentDate = ref(new Date());
|
||||
|
||||
// Use dates from v-model if provided, otherwise default to last 7 days
|
||||
const selectedStartDate = ref(
|
||||
dateRange.value?.[0]
|
||||
? startOfDay(dateRange.value[0])
|
||||
: startOfDay(subDays(currentDate.value, 6)) // LAST_7_DAYS
|
||||
);
|
||||
const selectedEndDate = ref(
|
||||
dateRange.value?.[1]
|
||||
? endOfDay(dateRange.value[1])
|
||||
: endOfDay(currentDate.value)
|
||||
);
|
||||
// Calendar month positioning (left and right calendars)
|
||||
// These control which months are displayed in the dual calendar view
|
||||
const startCurrentDate = ref(startOfMonth(selectedStartDate.value));
|
||||
const endCurrentDate = ref(
|
||||
isSameMonth(selectedStartDate.value, selectedEndDate.value)
|
||||
? startOfMonth(addMonths(selectedEndDate.value, 1)) // Same month: show next month on right (e.g., Jan 25-31 shows Jan + Feb)
|
||||
: startOfMonth(selectedEndDate.value) // Different months: show end month on right (e.g., Dec 5 - Jan 3 shows Dec + Jan)
|
||||
);
|
||||
const selectingEndDate = ref(false);
|
||||
const selectedRange = ref(rangeType.value || LAST_7_DAYS);
|
||||
const hoveredEndDate = ref(null);
|
||||
const monthOffset = ref(0);
|
||||
|
||||
const showMonthNavigation = computed(() =>
|
||||
isNavigableRange(selectedRange.value)
|
||||
);
|
||||
const canNavigateNext = computed(() => {
|
||||
if (!isNavigableRange(selectedRange.value)) return false;
|
||||
// Compare selected start to the current period's start to determine if we're in the past
|
||||
const currentRange = getActiveDateRange(
|
||||
selectedRange.value,
|
||||
currentDate.value
|
||||
);
|
||||
return selectedStartDate.value < currentRange.start;
|
||||
});
|
||||
|
||||
const navigationLabel = computed(() => {
|
||||
const range = selectedRange.value;
|
||||
if (range === DATE_RANGE_TYPES.MONTH_TO_DATE) {
|
||||
return new Intl.DateTimeFormat(navigator.language, {
|
||||
month: 'long',
|
||||
}).format(selectedStartDate.value);
|
||||
}
|
||||
if (range === DATE_RANGE_TYPES.THIS_WEEK) {
|
||||
const currentWeekRange = getActiveDateRange(range, currentDate.value);
|
||||
const isCurrentWeek =
|
||||
selectedStartDate.value.getTime() === currentWeekRange.start.getTime();
|
||||
if (isCurrentWeek) return null;
|
||||
const weekNumber = getWeek(selectedStartDate.value, { weekStartsOn: 1 });
|
||||
return t('DATE_PICKER.WEEK_NUMBER', { weekNumber });
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const manualStartDate = ref(selectedStartDate.value);
|
||||
const manualEndDate = ref(selectedEndDate.value);
|
||||
|
||||
// Watcher 1: Sync v-model props from parent component
|
||||
// Handles: URL params, parent component updates, rangeType changes
|
||||
watch(
|
||||
[rangeType, dateRange],
|
||||
([newRangeType, newDateRange]) => {
|
||||
if (newRangeType && newRangeType !== selectedRange.value) {
|
||||
selectedRange.value = newRangeType;
|
||||
monthOffset.value = 0;
|
||||
|
||||
// If rangeType changes without dateRange, recompute dates from the range
|
||||
if (!newDateRange && newRangeType !== CUSTOM_RANGE) {
|
||||
const activeDates = getActiveDateRange(newRangeType, currentDate.value);
|
||||
if (activeDates) {
|
||||
selectedStartDate.value = startOfDay(activeDates.start);
|
||||
selectedEndDate.value = endOfDay(activeDates.end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When parent provides new dateRange (e.g., from URL params)
|
||||
// Skip if navigating with arrows — offset controls dates in that case
|
||||
if (newDateRange?.[0] && newDateRange?.[1] && monthOffset.value === 0) {
|
||||
selectedStartDate.value = startOfDay(newDateRange[0]);
|
||||
selectedEndDate.value = endOfDay(newDateRange[1]);
|
||||
|
||||
// Update calendar to show the months of the new date range
|
||||
startCurrentDate.value = startOfMonth(newDateRange[0]);
|
||||
endCurrentDate.value = isSameMonth(newDateRange[0], newDateRange[1])
|
||||
? startOfMonth(addMonths(newDateRange[1], 1))
|
||||
: startOfMonth(newDateRange[1]);
|
||||
|
||||
// Recalculate offset so arrow navigation is relative to restored range
|
||||
// TODO: When offset resolves to 0 (current period), the end date may be
|
||||
// stale if the URL was saved on a previous day. "This month" / "This week"
|
||||
// should show up-to-today dates for the current period. For now, the stale
|
||||
// end date is shown until the user clicks an arrow or re-selects the range.
|
||||
if (isNavigableRange(selectedRange.value)) {
|
||||
const current = getActiveDateRange(
|
||||
selectedRange.value,
|
||||
currentDate.value
|
||||
);
|
||||
if (selectedRange.value === DATE_RANGE_TYPES.THIS_WEEK) {
|
||||
monthOffset.value = differenceInCalendarWeeks(
|
||||
newDateRange[0],
|
||||
current.start,
|
||||
{ weekStartsOn: 1 }
|
||||
);
|
||||
} else {
|
||||
monthOffset.value = differenceInCalendarMonths(
|
||||
newDateRange[0],
|
||||
current.start
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// Watcher 2: Keep manual input fields in sync with selected dates
|
||||
// Updates the input field values when dates change programmatically
|
||||
watch(
|
||||
[selectedStartDate, selectedEndDate],
|
||||
([newStart, newEnd]) => {
|
||||
manualStartDate.value = isValid(newStart)
|
||||
? newStart
|
||||
: selectedStartDate.value;
|
||||
manualEndDate.value = isValid(newEnd) ? newEnd : selectedEndDate.value;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const setDateRange = range => {
|
||||
selectedRange.value = range.value;
|
||||
monthOffset.value = 0;
|
||||
const { start, end } = getActiveDateRange(range.value, currentDate.value);
|
||||
selectedStartDate.value = start;
|
||||
selectedEndDate.value = end;
|
||||
|
||||
// Position calendar to show the months of the selected range
|
||||
startCurrentDate.value = startOfMonth(start);
|
||||
endCurrentDate.value = isSameMonth(start, end)
|
||||
? startOfMonth(addMonths(start, 1))
|
||||
: startOfMonth(end);
|
||||
};
|
||||
|
||||
const navigateMonth = direction => {
|
||||
monthOffset.value += direction === 'prev' ? -1 : 1;
|
||||
if (monthOffset.value > 0) monthOffset.value = 0;
|
||||
|
||||
const { start, end } = getRangeAtOffset(
|
||||
selectedRange.value,
|
||||
monthOffset.value,
|
||||
currentDate.value
|
||||
);
|
||||
selectedStartDate.value = start;
|
||||
selectedEndDate.value = end;
|
||||
|
||||
startCurrentDate.value = startOfMonth(start);
|
||||
endCurrentDate.value = isSameMonth(start, end)
|
||||
? startOfMonth(addMonths(start, 1))
|
||||
: startOfMonth(end);
|
||||
|
||||
emit('dateRangeChanged', [start, end, selectedRange.value]);
|
||||
};
|
||||
|
||||
const moveCalendar = (calendar, direction, period = MONTH) => {
|
||||
const { start, end } = moveCalendarDate(
|
||||
calendar,
|
||||
startCurrentDate.value,
|
||||
endCurrentDate.value,
|
||||
direction,
|
||||
period
|
||||
);
|
||||
|
||||
// Prevent calendar months from overlapping
|
||||
const monthDiff = differenceInCalendarMonths(end, start);
|
||||
if (monthDiff === 0) {
|
||||
// If they would be the same month, adjust the other calendar
|
||||
if (calendar === START_CALENDAR) {
|
||||
endCurrentDate.value = addMonths(start, 1);
|
||||
startCurrentDate.value = start;
|
||||
} else {
|
||||
startCurrentDate.value = subMonths(end, 1);
|
||||
endCurrentDate.value = end;
|
||||
}
|
||||
} else {
|
||||
startCurrentDate.value = start;
|
||||
endCurrentDate.value = end;
|
||||
}
|
||||
};
|
||||
|
||||
const selectDate = day => {
|
||||
selectedRange.value = CUSTOM_RANGE;
|
||||
monthOffset.value = 0;
|
||||
if (!selectingEndDate.value || day < selectedStartDate.value) {
|
||||
selectedStartDate.value = day;
|
||||
selectedEndDate.value = null;
|
||||
selectingEndDate.value = true;
|
||||
} else {
|
||||
selectedEndDate.value = day;
|
||||
selectingEndDate.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const setViewMode = (calendar, mode) => {
|
||||
selectedRange.value = CUSTOM_RANGE;
|
||||
calendarViews.value[calendar] = mode;
|
||||
};
|
||||
|
||||
const openCalendar = (index, calendarType, period = MONTH) => {
|
||||
const current =
|
||||
calendarType === START_CALENDAR
|
||||
? startCurrentDate.value
|
||||
: endCurrentDate.value;
|
||||
const newDate =
|
||||
period === MONTH
|
||||
? setMonth(startOfMonth(current), index)
|
||||
: setYear(current, index);
|
||||
if (calendarType === START_CALENDAR) {
|
||||
startCurrentDate.value = newDate;
|
||||
} else {
|
||||
endCurrentDate.value = newDate;
|
||||
}
|
||||
setViewMode(calendarType, period === MONTH ? WEEK : MONTH);
|
||||
};
|
||||
|
||||
const updateManualInput = (newDate, calendarType) => {
|
||||
if (calendarType === START_CALENDAR) {
|
||||
selectedStartDate.value = newDate;
|
||||
startCurrentDate.value = startOfMonth(newDate);
|
||||
} else {
|
||||
selectedEndDate.value = newDate;
|
||||
endCurrentDate.value = startOfMonth(newDate);
|
||||
}
|
||||
selectingEndDate.value = false;
|
||||
};
|
||||
|
||||
const handleManualInputError = message => {
|
||||
useAlert(message);
|
||||
};
|
||||
|
||||
const resetDatePicker = () => {
|
||||
// Calculate Last 7 days from today
|
||||
const startDate = startOfDay(subDays(currentDate.value, 6));
|
||||
const endDate = endOfDay(currentDate.value);
|
||||
|
||||
selectedStartDate.value = startDate;
|
||||
selectedEndDate.value = endDate;
|
||||
|
||||
// Position calendar to show the months of Last 7 days
|
||||
// Example: If today is Feb 5, Last 7 days = Jan 30 - Feb 5, so show Jan + Feb
|
||||
startCurrentDate.value = startOfMonth(startDate);
|
||||
endCurrentDate.value = isSameMonth(startDate, endDate)
|
||||
? startOfMonth(addMonths(startDate, 1))
|
||||
: startOfMonth(endDate);
|
||||
selectingEndDate.value = false;
|
||||
selectedRange.value = LAST_7_DAYS;
|
||||
monthOffset.value = 0;
|
||||
calendarViews.value = { start: WEEK, end: WEEK };
|
||||
};
|
||||
|
||||
const emitDateRange = () => {
|
||||
if (!isValid(selectedStartDate.value) || !isValid(selectedEndDate.value)) {
|
||||
useAlert('Please select a valid time range');
|
||||
} else {
|
||||
showDatePicker.value = false;
|
||||
emit('dateRangeChanged', [
|
||||
selectedStartDate.value,
|
||||
selectedEndDate.value,
|
||||
selectedRange.value,
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
// Called when picker opens - positions calendar to show selected date range
|
||||
// Fixes issue where calendar showed wrong months when loaded from URL params
|
||||
const initializeCalendarMonths = () => {
|
||||
if (selectedStartDate.value && selectedEndDate.value) {
|
||||
startCurrentDate.value = startOfMonth(selectedStartDate.value);
|
||||
endCurrentDate.value = isSameMonth(
|
||||
selectedStartDate.value,
|
||||
selectedEndDate.value
|
||||
)
|
||||
? startOfMonth(addMonths(selectedEndDate.value, 1))
|
||||
: startOfMonth(selectedEndDate.value);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleDatePicker = () => {
|
||||
showDatePicker.value = !showDatePicker.value;
|
||||
if (showDatePicker.value) initializeCalendarMonths();
|
||||
};
|
||||
|
||||
const closeDatePicker = () => {
|
||||
if (isValid(selectedStartDate.value) && isValid(selectedEndDate.value)) {
|
||||
emitDateRange();
|
||||
} else {
|
||||
showDatePicker.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative flex-shrink-0 font-inter">
|
||||
<DatePickerButton
|
||||
:selected-start-date="selectedStartDate"
|
||||
:selected-end-date="selectedEndDate"
|
||||
:selected-range="selectedRange"
|
||||
:show-month-navigation="showMonthNavigation"
|
||||
:can-navigate-next="canNavigateNext"
|
||||
:navigation-label="navigationLabel"
|
||||
@open="toggleDatePicker"
|
||||
@navigate-month="navigateMonth"
|
||||
/>
|
||||
<div
|
||||
v-if="showDatePicker"
|
||||
v-on-clickaway="closeDatePicker"
|
||||
class="flex absolute top-9 ltr:left-0 rtl:right-0 z-30 shadow-md select-none w-[880px] rounded-2xl bg-n-alpha-3 backdrop-blur-[100px] border-0 outline outline-1 outline-n-container"
|
||||
>
|
||||
<CalendarDateRange
|
||||
:selected-range="selectedRange"
|
||||
@set-range="setDateRange"
|
||||
/>
|
||||
<div
|
||||
class="flex flex-col w-[680px] ltr:border-l rtl:border-r border-n-strong"
|
||||
>
|
||||
<div class="flex justify-around h-fit">
|
||||
<!-- Calendars for Start and End Dates -->
|
||||
<div
|
||||
v-for="calendar in [START_CALENDAR, END_CALENDAR]"
|
||||
:key="`${calendar}-calendar`"
|
||||
class="flex flex-col items-center"
|
||||
>
|
||||
<CalendarDateInput
|
||||
:calendar-type="calendar"
|
||||
:date-value="
|
||||
calendar === START_CALENDAR ? manualStartDate : manualEndDate
|
||||
"
|
||||
:compare-date="
|
||||
calendar === START_CALENDAR ? manualEndDate : manualStartDate
|
||||
"
|
||||
:is-disabled="selectedRange !== CUSTOM_RANGE"
|
||||
@update="
|
||||
calendar === START_CALENDAR
|
||||
? (manualStartDate = $event)
|
||||
: (manualEndDate = $event)
|
||||
"
|
||||
@validate="updateManualInput($event, calendar)"
|
||||
@error="handleManualInputError($event)"
|
||||
/>
|
||||
<div class="py-5 border-b border-n-strong">
|
||||
<div
|
||||
class="flex flex-col items-center gap-2 px-5 min-w-[340px] max-h-[352px]"
|
||||
:class="
|
||||
calendar === START_CALENDAR &&
|
||||
'ltr:border-r rtl:border-l border-n-strong'
|
||||
"
|
||||
>
|
||||
<CalendarYear
|
||||
v-if="calendarViews[calendar] === YEAR"
|
||||
:calendar-type="calendar"
|
||||
:start-current-date="startCurrentDate"
|
||||
:end-current-date="endCurrentDate"
|
||||
@select-year="openCalendar($event, calendar, YEAR)"
|
||||
/>
|
||||
<CalendarMonth
|
||||
v-else-if="calendarViews[calendar] === MONTH"
|
||||
:calendar-type="calendar"
|
||||
:start-current-date="startCurrentDate"
|
||||
:end-current-date="endCurrentDate"
|
||||
@select-month="openCalendar($event, calendar)"
|
||||
@set-view="setViewMode"
|
||||
@prev="moveCalendar(calendar, 'prev', YEAR)"
|
||||
@next="moveCalendar(calendar, 'next', YEAR)"
|
||||
/>
|
||||
<CalendarWeek
|
||||
v-else-if="calendarViews[calendar] === WEEK"
|
||||
:calendar-type="calendar"
|
||||
:current-date="currentDate"
|
||||
:start-current-date="startCurrentDate"
|
||||
:end-current-date="endCurrentDate"
|
||||
:selected-start-date="selectedStartDate"
|
||||
:selected-end-date="selectedEndDate"
|
||||
:selecting-end-date="selectingEndDate"
|
||||
:hovered-end-date="hoveredEndDate"
|
||||
@update-hovered-end-date="hoveredEndDate = $event"
|
||||
@select-date="selectDate"
|
||||
@set-view="setViewMode"
|
||||
@prev="moveCalendar(calendar, 'prev')"
|
||||
@next="moveCalendar(calendar, 'next')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CalendarFooter @change="emitDateRange" @clear="resetDatePicker" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,77 @@
|
||||
<script setup>
|
||||
import { CALENDAR_PERIODS } from '../helpers/DatePickerHelper';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
calendarType: {
|
||||
type: String,
|
||||
default: 'start',
|
||||
},
|
||||
firstButtonLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
buttonLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
viewMode: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['prev', 'next', 'setView']);
|
||||
|
||||
const { YEAR } = CALENDAR_PERIODS;
|
||||
|
||||
const onClickPrev = type => {
|
||||
emit('prev', type);
|
||||
};
|
||||
|
||||
const onClickNext = type => {
|
||||
emit('next', type);
|
||||
};
|
||||
|
||||
const onClickSetView = (type, mode) => {
|
||||
emit('setView', type, mode);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-start justify-between w-full h-9">
|
||||
<NextButton
|
||||
slate
|
||||
ghost
|
||||
xs
|
||||
icon="i-lucide-chevron-left"
|
||||
class="rtl:rotate-180"
|
||||
@click.stop="onClickPrev(calendarType)"
|
||||
/>
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
v-if="firstButtonLabel"
|
||||
class="p-0 text-sm font-medium text-center text-n-slate-12 hover:text-n-brand"
|
||||
@click.stop="onClickSetView(calendarType, viewMode)"
|
||||
>
|
||||
{{ firstButtonLabel }}
|
||||
</button>
|
||||
<button
|
||||
v-if="buttonLabel"
|
||||
class="p-0 text-sm font-medium text-center text-n-slate-12"
|
||||
:class="{ 'hover:text-n-brand': viewMode }"
|
||||
@click.stop="onClickSetView(calendarType, YEAR)"
|
||||
>
|
||||
{{ buttonLabel }}
|
||||
</button>
|
||||
</div>
|
||||
<NextButton
|
||||
slate
|
||||
ghost
|
||||
xs
|
||||
icon="i-lucide-chevron-right"
|
||||
class="rtl:rotate-180"
|
||||
@click.stop="onClickNext(calendarType)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,74 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { parse, isValid, isAfter, isBefore } from 'date-fns';
|
||||
import {
|
||||
getIntlDateFormatForLocale,
|
||||
CALENDAR_TYPES,
|
||||
} from '../helpers/DatePickerHelper';
|
||||
|
||||
const props = defineProps({
|
||||
calendarType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
dateValue: Date,
|
||||
compareDate: Date,
|
||||
isDisabled: Boolean,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update', 'validate', 'error']);
|
||||
|
||||
const { START_CALENDAR, END_CALENDAR } = CALENDAR_TYPES;
|
||||
|
||||
const dateFormat = computed(() => getIntlDateFormatForLocale()?.toUpperCase());
|
||||
|
||||
const localDateValue = computed({
|
||||
get: () => props.dateValue?.toLocaleDateString(navigator.language) || '',
|
||||
set: newValue => {
|
||||
const format = getIntlDateFormatForLocale();
|
||||
const parsedDate = parse(newValue, format, new Date());
|
||||
if (isValid(parsedDate)) {
|
||||
emit('update', parsedDate);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const validateDate = () => {
|
||||
if (!isValid(props.dateValue)) {
|
||||
emit('error', `Please enter the date in valid format: ${dateFormat.value}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { calendarType, compareDate, dateValue } = props;
|
||||
const isStartCalendar = calendarType === START_CALENDAR;
|
||||
const isEndCalendar = calendarType === END_CALENDAR;
|
||||
|
||||
if (compareDate && isStartCalendar && isAfter(dateValue, compareDate)) {
|
||||
emit('error', 'Start date must be before the end date.');
|
||||
} else if (compareDate && isEndCalendar && isBefore(dateValue, compareDate)) {
|
||||
emit('error', 'End date must be after the start date.');
|
||||
} else {
|
||||
emit('validate', dateValue);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-[82px] flex flex-col items-start px-5 gap-1.5 pt-4 w-full">
|
||||
<span class="text-sm font-medium text-n-slate-12">
|
||||
{{
|
||||
calendarType === START_CALENDAR
|
||||
? $t('DATE_PICKER.DATE_RANGE_INPUT.START')
|
||||
: $t('DATE_PICKER.DATE_RANGE_INPUT.END')
|
||||
}}
|
||||
</span>
|
||||
<input
|
||||
v-model="localDateValue"
|
||||
type="text"
|
||||
class="!text-sm !mb-0 disabled:!outline-n-strong"
|
||||
:placeholder="dateFormat"
|
||||
:disabled="isDisabled"
|
||||
@keypress.enter="validateDate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script setup>
|
||||
import { dateRanges } from '../helpers/DatePickerHelper';
|
||||
|
||||
defineProps({
|
||||
selectedRange: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['setRange']);
|
||||
|
||||
const setDateRange = range => {
|
||||
emit('setRange', range);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-[200px] flex flex-col items-start">
|
||||
<h4
|
||||
class="w-full px-5 py-4 text-xs font-bold capitalize text-start text-n-slate-10"
|
||||
>
|
||||
{{ $t('DATE_PICKER.DATE_RANGE_OPTIONS.TITLE') }}
|
||||
</h4>
|
||||
<div class="flex flex-col items-start w-full">
|
||||
<template v-for="range in dateRanges" :key="range.label">
|
||||
<div v-if="range.separator" class="w-full border-t border-n-strong" />
|
||||
<button
|
||||
class="w-full px-5 py-3 text-sm font-medium truncate border-none rounded-none text-start hover:bg-n-alpha-2 dark:hover:bg-n-solid-3"
|
||||
:class="
|
||||
range.value === selectedRange
|
||||
? 'text-n-slate-12 bg-n-alpha-1 dark:bg-n-solid-active'
|
||||
: 'text-n-slate-12'
|
||||
"
|
||||
@click="setDateRange(range)"
|
||||
>
|
||||
{{ $t(range.label) }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup>
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const emit = defineEmits(['clear', 'change']);
|
||||
|
||||
const onClickClear = () => {
|
||||
emit('clear');
|
||||
};
|
||||
|
||||
const onClickApply = () => {
|
||||
emit('change');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-[56px] flex justify-between gap-2 px-2 py-3 items-center">
|
||||
<NextButton
|
||||
slate
|
||||
ghost
|
||||
sm
|
||||
:label="$t('DATE_PICKER.CLEAR_BUTTON')"
|
||||
@click="onClickClear"
|
||||
/>
|
||||
<NextButton
|
||||
sm
|
||||
:label="$t('DATE_PICKER.APPLY_BUTTON')"
|
||||
@click="onClickApply"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,87 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { format, getMonth, setMonth, startOfMonth } from 'date-fns';
|
||||
import {
|
||||
yearName,
|
||||
CALENDAR_TYPES,
|
||||
CALENDAR_PERIODS,
|
||||
} from '../helpers/DatePickerHelper';
|
||||
|
||||
import CalendarAction from './CalendarAction.vue';
|
||||
|
||||
const props = defineProps({
|
||||
calendarType: {
|
||||
type: String,
|
||||
default: 'start',
|
||||
},
|
||||
startCurrentDate: Date,
|
||||
endCurrentDate: Date,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['selectMonth', 'prev', 'next', 'setView']);
|
||||
const { START_CALENDAR } = CALENDAR_TYPES;
|
||||
const { MONTH, YEAR } = CALENDAR_PERIODS;
|
||||
|
||||
const months = Array.from({ length: 12 }, (_, index) =>
|
||||
format(setMonth(startOfMonth(new Date()), index), 'MMM')
|
||||
);
|
||||
|
||||
const activeMonthIndex = computed(() => {
|
||||
const date =
|
||||
props.calendarType === START_CALENDAR
|
||||
? props.startCurrentDate
|
||||
: props.endCurrentDate;
|
||||
return getMonth(date);
|
||||
});
|
||||
|
||||
const setViewMode = (type, mode) => {
|
||||
emit('setView', type, mode);
|
||||
};
|
||||
|
||||
const onClickPrev = () => {
|
||||
emit('prev');
|
||||
};
|
||||
|
||||
const onClickNext = () => {
|
||||
emit('next');
|
||||
};
|
||||
|
||||
const selectMonth = index => {
|
||||
emit('selectMonth', index);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col w-full gap-2 max-h-[312px]">
|
||||
<CalendarAction
|
||||
:view-mode="YEAR"
|
||||
:calendar-type="calendarType"
|
||||
:button-label="
|
||||
yearName(
|
||||
calendarType === START_CALENDAR ? startCurrentDate : endCurrentDate,
|
||||
MONTH
|
||||
)
|
||||
"
|
||||
@set-view="setViewMode"
|
||||
@prev="onClickPrev"
|
||||
@next="onClickNext"
|
||||
/>
|
||||
|
||||
<div class="grid w-full grid-cols-3 gap-x-3 gap-y-2 auto-rows-[61px]">
|
||||
<button
|
||||
v-for="(month, index) in months"
|
||||
:key="index"
|
||||
class="p-2 text-sm font-medium text-center text-n-slate-12 w-[92px] h-10 rounded-lg py-2.5 px-2"
|
||||
:class="{
|
||||
'bg-n-brand text-white hover:bg-n-blue-10':
|
||||
index === activeMonthIndex,
|
||||
'hover:bg-n-alpha-2 dark:hover:bg-n-solid-3':
|
||||
index !== activeMonthIndex,
|
||||
}"
|
||||
@click.stop="selectMonth(index)"
|
||||
>
|
||||
{{ month }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,185 @@
|
||||
<script setup>
|
||||
import { startOfDay } from 'date-fns';
|
||||
import {
|
||||
monthName,
|
||||
yearName,
|
||||
getWeeksForMonth,
|
||||
isToday,
|
||||
dayIsInRange,
|
||||
isCurrentMonth,
|
||||
isLastDayOfMonth,
|
||||
isHoveringDayInRange,
|
||||
isHoveringNextDayInRange,
|
||||
CALENDAR_TYPES,
|
||||
CALENDAR_PERIODS,
|
||||
} from '../helpers/DatePickerHelper';
|
||||
|
||||
import CalendarWeekLabel from './CalendarWeekLabel.vue';
|
||||
import CalendarAction from './CalendarAction.vue';
|
||||
|
||||
const props = defineProps({
|
||||
calendarType: {
|
||||
type: String,
|
||||
default: 'start',
|
||||
},
|
||||
currentDate: Date,
|
||||
startCurrentDate: Date,
|
||||
endCurrentDate: Date,
|
||||
selectedStartDate: Date,
|
||||
selectingEndDate: Boolean,
|
||||
selectedEndDate: Date,
|
||||
hoveredEndDate: Date,
|
||||
minDate: {
|
||||
type: Date,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'updateHoveredEndDate',
|
||||
'selectDate',
|
||||
'prev',
|
||||
'next',
|
||||
'setView',
|
||||
]);
|
||||
|
||||
const { START_CALENDAR } = CALENDAR_TYPES;
|
||||
const { MONTH } = CALENDAR_PERIODS;
|
||||
|
||||
const isDayDisabled = day => {
|
||||
if (!props.minDate) return false;
|
||||
return startOfDay(day) < startOfDay(props.minDate);
|
||||
};
|
||||
|
||||
const emitHoveredEndDate = day => {
|
||||
if (isDayDisabled(day)) return;
|
||||
emit('updateHoveredEndDate', day);
|
||||
};
|
||||
|
||||
const emitSelectDate = day => {
|
||||
if (isDayDisabled(day)) return;
|
||||
emit('selectDate', day);
|
||||
};
|
||||
const onClickPrev = () => {
|
||||
emit('prev');
|
||||
};
|
||||
|
||||
const onClickNext = () => {
|
||||
emit('next');
|
||||
};
|
||||
|
||||
const setViewMode = (type, mode) => {
|
||||
emit('setView', type, mode);
|
||||
};
|
||||
|
||||
const weeks = calendarType => {
|
||||
return getWeeksForMonth(
|
||||
calendarType === START_CALENDAR
|
||||
? props.startCurrentDate
|
||||
: props.endCurrentDate
|
||||
);
|
||||
};
|
||||
|
||||
const isSelectedStartOrEndDate = day => {
|
||||
return (
|
||||
dayIsInRange(day, props.selectedStartDate, props.selectedStartDate) ||
|
||||
dayIsInRange(day, props.selectedEndDate, props.selectedEndDate)
|
||||
);
|
||||
};
|
||||
|
||||
const isInRange = day => {
|
||||
return dayIsInRange(day, props.selectedStartDate, props.selectedEndDate);
|
||||
};
|
||||
|
||||
const isInCurrentMonth = day => {
|
||||
return isCurrentMonth(
|
||||
day,
|
||||
props.calendarType === START_CALENDAR
|
||||
? props.startCurrentDate
|
||||
: props.endCurrentDate
|
||||
);
|
||||
};
|
||||
|
||||
const isHoveringInRange = day => {
|
||||
return isHoveringDayInRange(
|
||||
day,
|
||||
props.selectedStartDate,
|
||||
props.selectingEndDate,
|
||||
props.hoveredEndDate
|
||||
);
|
||||
};
|
||||
|
||||
const isNextDayInRange = day => {
|
||||
return isHoveringNextDayInRange(
|
||||
day,
|
||||
props.selectedStartDate,
|
||||
props.selectedEndDate,
|
||||
props.hoveredEndDate
|
||||
);
|
||||
};
|
||||
|
||||
const dayClasses = day => ({
|
||||
'text-n-slate-10 pointer-events-none': !isInCurrentMonth(day),
|
||||
'text-n-slate-10 pointer-events-none opacity-40':
|
||||
isInCurrentMonth(day) && isDayDisabled(day),
|
||||
'text-n-slate-12 hover:text-n-slate-12 hover:bg-n-blue-6 dark:hover:bg-n-blue-7':
|
||||
isInCurrentMonth(day) && !isDayDisabled(day),
|
||||
'bg-n-brand text-white':
|
||||
isSelectedStartOrEndDate(day) && isInCurrentMonth(day),
|
||||
'bg-n-blue-4 dark:bg-n-blue-5':
|
||||
(isInRange(day) || isHoveringInRange(day)) &&
|
||||
!isSelectedStartOrEndDate(day) &&
|
||||
isInCurrentMonth(day),
|
||||
'outline outline-1 outline-n-blue-8 -outline-offset-1 !text-n-blue-11':
|
||||
isToday(props.currentDate, day) && !isSelectedStartOrEndDate(day),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col w-full gap-2 max-h-[312px]">
|
||||
<CalendarAction
|
||||
:view-mode="MONTH"
|
||||
:calendar-type="calendarType"
|
||||
:first-button-label="
|
||||
monthName(
|
||||
calendarType === START_CALENDAR ? startCurrentDate : endCurrentDate
|
||||
)
|
||||
"
|
||||
:button-label="
|
||||
yearName(
|
||||
calendarType === START_CALENDAR ? startCurrentDate : endCurrentDate
|
||||
)
|
||||
"
|
||||
@prev="onClickPrev"
|
||||
@next="onClickNext"
|
||||
@set-view="setViewMode"
|
||||
/>
|
||||
<CalendarWeekLabel />
|
||||
<div
|
||||
v-for="week in weeks(calendarType)"
|
||||
:key="week[0].getTime()"
|
||||
class="grid max-w-md grid-cols-7 gap-2 mx-auto overflow-hidden rounded-lg"
|
||||
>
|
||||
<div
|
||||
v-for="day in week"
|
||||
:key="day.getTime()"
|
||||
class="flex relative items-center justify-center w-9 h-8 py-1.5 px-2 font-medium text-sm rounded-lg cursor-pointer"
|
||||
:class="dayClasses(day)"
|
||||
@mouseenter="emitHoveredEndDate(day)"
|
||||
@mouseleave="emitHoveredEndDate(null)"
|
||||
@click="emitSelectDate(day)"
|
||||
>
|
||||
{{ day.getDate() }}
|
||||
<span
|
||||
v-if="
|
||||
(isInRange(day) || isHoveringInRange(day)) &&
|
||||
isNextDayInRange(day) &&
|
||||
!isLastDayOfMonth(day) &&
|
||||
isInCurrentMonth(day)
|
||||
"
|
||||
class="absolute bottom-0 w-6 h-8 ltr:-right-4 rtl:-left-4 bg-n-blue-4 dark:bg-n-blue-5 -z-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script setup>
|
||||
import { calendarWeeks } from '../helpers/DatePickerHelper';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="max-w-md mx-auto grid grid-cols-7 gap-2">
|
||||
<div
|
||||
v-for="day in calendarWeeks"
|
||||
:key="day.id"
|
||||
class="flex items-center justify-center font-medium text-sm w-9 h-7 py-1.5 px-2"
|
||||
>
|
||||
{{ day.label }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,86 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { getYear, addYears, subYears } from 'date-fns';
|
||||
import { CALENDAR_TYPES } from '../helpers/DatePickerHelper';
|
||||
|
||||
import CalendarAction from './CalendarAction.vue';
|
||||
|
||||
const props = defineProps({
|
||||
calendarType: {
|
||||
type: String,
|
||||
default: 'start',
|
||||
},
|
||||
startCurrentDate: Date,
|
||||
endCurrentDate: Date,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['selectYear']);
|
||||
|
||||
const { START_CALENDAR } = CALENDAR_TYPES;
|
||||
|
||||
const calculateStartYear = date => {
|
||||
const year = getYear(date);
|
||||
return year - (year % 10); // Align with the beginning of a decade
|
||||
};
|
||||
|
||||
const startYear = ref(
|
||||
calculateStartYear(
|
||||
props.calendarType === START_CALENDAR
|
||||
? props.startCurrentDate
|
||||
: props.endCurrentDate
|
||||
)
|
||||
);
|
||||
|
||||
const years = computed(() =>
|
||||
Array.from({ length: 10 }, (_, i) => startYear.value + i)
|
||||
);
|
||||
|
||||
const firstYear = computed(() => years.value[0]);
|
||||
const lastYear = computed(() => years.value[years.value.length - 1]);
|
||||
|
||||
const activeYear = computed(() => {
|
||||
const date =
|
||||
props.calendarType === START_CALENDAR
|
||||
? props.startCurrentDate
|
||||
: props.endCurrentDate;
|
||||
return getYear(date);
|
||||
});
|
||||
|
||||
const onClickPrev = () => {
|
||||
startYear.value = subYears(new Date(startYear.value, 0, 1), 10).getFullYear();
|
||||
};
|
||||
|
||||
const onClickNext = () => {
|
||||
startYear.value = addYears(new Date(startYear.value, 0, 1), 10).getFullYear();
|
||||
};
|
||||
|
||||
const selectYear = year => {
|
||||
emit('selectYear', year);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col w-full gap-2 max-h-[312px]">
|
||||
<CalendarAction
|
||||
:calendar-type="calendarType"
|
||||
:button-label="`${firstYear} - ${lastYear}`"
|
||||
@prev="onClickPrev"
|
||||
@next="onClickNext"
|
||||
/>
|
||||
|
||||
<div class="grid grid-cols-2 gap-x-3 gap-y-2 w-full auto-rows-[47px]">
|
||||
<button
|
||||
v-for="year in years"
|
||||
:key="year"
|
||||
class="p-2 text-sm font-medium text-center text-n-slate-12 w-[144px] h-10 rounded-lg py-2.5 px-2"
|
||||
:class="{
|
||||
'bg-n-brand text-white hover:bg-n-blue-10': year === activeYear,
|
||||
'hover:bg-n-alpha-2 dark:hover:bg-n-solid-3': year !== activeYear,
|
||||
}"
|
||||
@click.stop="selectYear(year)"
|
||||
>
|
||||
{{ year }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,102 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { dateRanges } from '../helpers/DatePickerHelper';
|
||||
import { format, isSameYear, isValid } from 'date-fns';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
selectedStartDate: Date,
|
||||
selectedEndDate: Date,
|
||||
selectedRange: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
showMonthNavigation: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
canNavigateNext: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
navigationLabel: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['open', 'navigateMonth']);
|
||||
|
||||
const formatDateRange = computed(() => {
|
||||
const startDate = props.selectedStartDate;
|
||||
const endDate = props.selectedEndDate;
|
||||
|
||||
if (!isValid(startDate) || !isValid(endDate)) {
|
||||
return 'Select a date range';
|
||||
}
|
||||
|
||||
const crossesYears = !isSameYear(startDate, endDate);
|
||||
|
||||
// Always show years when crossing year boundaries
|
||||
if (crossesYears) {
|
||||
return `${format(startDate, 'MMM d, yyyy')} - ${format(endDate, 'MMM d, yyyy')}`;
|
||||
}
|
||||
|
||||
// For same year, always show the year for clarity
|
||||
return `${format(startDate, 'MMM d')} - ${format(endDate, 'MMM d, yyyy')}`;
|
||||
});
|
||||
|
||||
const activeDateRange = computed(
|
||||
() => dateRanges.find(range => range.value === props.selectedRange).label
|
||||
);
|
||||
|
||||
const openDatePicker = () => {
|
||||
emit('open');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center gap-1">
|
||||
<button
|
||||
class="inline-flex relative items-center rounded-lg gap-2 py-1.5 px-3 h-8 bg-n-alpha-2 hover:bg-n-alpha-1 active:bg-n-alpha-1 flex-shrink-0"
|
||||
@click="openDatePicker"
|
||||
>
|
||||
<Icon
|
||||
icon="i-lucide-calendar-range"
|
||||
class="text-n-slate-11 size-3.5 flex-shrink-0"
|
||||
/>
|
||||
<span class="text-sm font-medium text-n-slate-12 truncate">
|
||||
{{ navigationLabel || $t(activeDateRange) }}
|
||||
</span>
|
||||
<span class="text-sm font-medium text-n-slate-11 truncate">
|
||||
{{ formatDateRange }}
|
||||
</span>
|
||||
<Icon
|
||||
icon="i-lucide-chevron-down"
|
||||
class="text-n-slate-12 size-4 flex-shrink-0"
|
||||
/>
|
||||
</button>
|
||||
<NextButton
|
||||
v-if="showMonthNavigation"
|
||||
v-tooltip.top="$t('DATE_PICKER.PREVIOUS_PERIOD')"
|
||||
slate
|
||||
faded
|
||||
sm
|
||||
icon="i-lucide-chevron-left"
|
||||
class="rtl:rotate-180"
|
||||
@click="emit('navigateMonth', 'prev')"
|
||||
/>
|
||||
<NextButton
|
||||
v-if="showMonthNavigation"
|
||||
v-tooltip.top="$t('DATE_PICKER.NEXT_PERIOD')"
|
||||
slate
|
||||
faded
|
||||
sm
|
||||
icon="i-lucide-chevron-right"
|
||||
class="rtl:rotate-180"
|
||||
:disabled="!canNavigateNext"
|
||||
@click="emit('navigateMonth', 'next')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,430 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import TabBar from 'dashboard/components-next/tabbar/TabBar.vue';
|
||||
import {
|
||||
TIME_COLUMNS,
|
||||
TIME_PERIODS,
|
||||
TIME_FORMATS,
|
||||
} from '../helpers/DatePickerHelper';
|
||||
|
||||
const props = defineProps({
|
||||
minTime: { type: Date, default: null },
|
||||
});
|
||||
|
||||
const model = defineModel({
|
||||
type: Object,
|
||||
default: () => ({ hour: 9, minute: 0, second: 0 }),
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { HOUR, HOUR_12, MINUTE, SECOND, PERIOD } = TIME_COLUMNS;
|
||||
const { AM, PM } = TIME_PERIODS;
|
||||
const { H24, H12 } = TIME_FORMATS;
|
||||
|
||||
const ITEM_HEIGHT = 40;
|
||||
const CENTER_ROW = 2;
|
||||
const VISIBLE_ROWS = 5;
|
||||
const SNAP_DELAY = 120;
|
||||
const SEPARATOR = ':';
|
||||
|
||||
const is24HourFormat = ref(false);
|
||||
const selectedTime = reactive({
|
||||
hour: model.value.hour,
|
||||
minute: model.value.minute,
|
||||
second: model.value.second ?? 0,
|
||||
});
|
||||
const activeIndex = reactive({});
|
||||
const dragOffset = reactive({});
|
||||
const interactionMode = reactive({});
|
||||
const touchStartY = {};
|
||||
const snapTimers = {};
|
||||
const columnRefs = reactive({});
|
||||
const focusedColumn = ref(null);
|
||||
|
||||
const period = computed(() => (selectedTime.hour >= 12 ? PM : AM));
|
||||
const showPeriod = computed(() => !is24HourFormat.value);
|
||||
|
||||
const minBound = computed(() => {
|
||||
if (!props.minTime) return null;
|
||||
const d = props.minTime;
|
||||
return { h: d.getHours(), m: d.getMinutes(), s: d.getSeconds() };
|
||||
});
|
||||
|
||||
const toTimeNumber = (h, m, s) => h * 3600 + m * 60 + s;
|
||||
|
||||
const convertTo12Hour = hour => {
|
||||
if (hour === 0) return 12;
|
||||
return hour > 12 ? hour - 12 : hour;
|
||||
};
|
||||
|
||||
const convertTo24Hour = (hour12, timePeriod) => {
|
||||
if (timePeriod === AM) return hour12 === 12 ? 0 : hour12;
|
||||
return hour12 === 12 ? 12 : hour12 + 12;
|
||||
};
|
||||
|
||||
const isItemDisabled = (key, val) => {
|
||||
const mb = minBound.value;
|
||||
if (!mb) return false;
|
||||
const minVal = toTimeNumber(mb.h, mb.m, mb.s);
|
||||
const { hour, minute } = selectedTime;
|
||||
if (key === HOUR) return toTimeNumber(val, 0, 0) < toTimeNumber(mb.h, 0, 0);
|
||||
if (key === MINUTE) return toTimeNumber(hour, val, 0) < minVal;
|
||||
if (key === SECOND) return toTimeNumber(hour, minute, val) < minVal;
|
||||
return false;
|
||||
};
|
||||
|
||||
const formatLabel = value => String(value).padStart(2, '0');
|
||||
|
||||
const buildItems = (count, key) =>
|
||||
Array.from({ length: count }, (_, i) => ({
|
||||
value: i,
|
||||
label: formatLabel(i),
|
||||
disabled: isItemDisabled(key, i),
|
||||
}));
|
||||
|
||||
const buildHour12Items = () =>
|
||||
Array.from({ length: 12 }, (_, i) => {
|
||||
const display = i === 0 ? 12 : i;
|
||||
return {
|
||||
value: display,
|
||||
label: formatLabel(display),
|
||||
disabled: isItemDisabled(HOUR, convertTo24Hour(display, period.value)),
|
||||
};
|
||||
});
|
||||
|
||||
const periodItems = computed(() => [
|
||||
{ value: AM, label: AM, disabled: minBound.value?.h >= 12 },
|
||||
{ value: PM, label: PM, disabled: false },
|
||||
]);
|
||||
|
||||
const columns = computed(() => [
|
||||
{
|
||||
key: is24HourFormat.value ? HOUR : HOUR_12,
|
||||
label: t('DATE_PICKER.HOUR'),
|
||||
items: is24HourFormat.value ? buildItems(24, HOUR) : buildHour12Items(),
|
||||
},
|
||||
{
|
||||
key: MINUTE,
|
||||
label: t('DATE_PICKER.MINUTE'),
|
||||
items: buildItems(60, MINUTE),
|
||||
},
|
||||
{
|
||||
key: SECOND,
|
||||
label: t('DATE_PICKER.SECOND'),
|
||||
items: buildItems(60, SECOND),
|
||||
},
|
||||
{ key: PERIOD, label: '', items: periodItems.value },
|
||||
]);
|
||||
|
||||
const findColumn = key => columns.value.find(c => c.key === key);
|
||||
|
||||
const findNearestEnabled = (items, index) => {
|
||||
for (let d = 0; d < items.length; d += 1) {
|
||||
if (index + d < items.length && !items[index + d].disabled)
|
||||
return index + d;
|
||||
if (index - d >= 0 && !items[index - d].disabled) return index - d;
|
||||
}
|
||||
return index;
|
||||
};
|
||||
|
||||
const getActiveIndex = key => {
|
||||
if (key === HOUR) return selectedTime.hour;
|
||||
if (key === HOUR_12) {
|
||||
const h = convertTo12Hour(selectedTime.hour);
|
||||
return h === 12 ? 0 : h;
|
||||
}
|
||||
if (key === PERIOD) return period.value === AM ? 0 : 1;
|
||||
return selectedTime[key];
|
||||
};
|
||||
|
||||
const getColumnTranslateY = key => {
|
||||
const idx = -(activeIndex[key] ?? 0) * ITEM_HEIGHT;
|
||||
return CENTER_ROW * ITEM_HEIGHT + idx + (dragOffset[key] ?? 0);
|
||||
};
|
||||
|
||||
const getMaxDragOffset = key => {
|
||||
const column = findColumn(key);
|
||||
if (!column) return { maxDown: 0, maxUp: 0 };
|
||||
const currentIdx = activeIndex[key] ?? 0;
|
||||
const firstEnabled = column.items.findIndex(i => !i.disabled);
|
||||
const lastEnabled = column.items.findLastIndex(i => !i.disabled);
|
||||
if (firstEnabled === -1) return { maxDown: 0, maxUp: 0 };
|
||||
return {
|
||||
maxDown: (currentIdx - firstEnabled) * ITEM_HEIGHT,
|
||||
maxUp: (lastEnabled - currentIdx) * ITEM_HEIGHT,
|
||||
};
|
||||
};
|
||||
|
||||
const clampDragOffset = (key, rawOffset) => {
|
||||
const { maxDown, maxUp } = getMaxDragOffset(key);
|
||||
return Math.max(-maxUp, Math.min(maxDown, rawOffset));
|
||||
};
|
||||
|
||||
let lastEmittedSignature = '';
|
||||
const emitTimeValue = () => {
|
||||
const sig = `${selectedTime.hour}:${selectedTime.minute}:${selectedTime.second}`;
|
||||
if (sig === lastEmittedSignature) return;
|
||||
lastEmittedSignature = sig;
|
||||
model.value = { ...selectedTime };
|
||||
};
|
||||
|
||||
const applySelection = (key, item) => {
|
||||
if (key === HOUR_12) {
|
||||
selectedTime.hour = convertTo24Hour(item.value, period.value);
|
||||
} else if (key === PERIOD) {
|
||||
selectedTime.hour = convertTo24Hour(
|
||||
convertTo12Hour(selectedTime.hour),
|
||||
item.value
|
||||
);
|
||||
} else {
|
||||
selectedTime[key] = item.value;
|
||||
}
|
||||
emitTimeValue();
|
||||
};
|
||||
|
||||
const selectIndex = (key, targetIndex) => {
|
||||
const column = findColumn(key);
|
||||
if (!column) return;
|
||||
const clamped = Math.max(0, Math.min(targetIndex, column.items.length - 1));
|
||||
const resolved = column.items[clamped]?.disabled
|
||||
? findNearestEnabled(column.items, clamped)
|
||||
: clamped;
|
||||
activeIndex[key] = resolved;
|
||||
const item = column.items[resolved];
|
||||
if (item) applySelection(key, item);
|
||||
};
|
||||
|
||||
const selectByValue = (key, value) => {
|
||||
const column = findColumn(key);
|
||||
if (!column) return;
|
||||
const idx = column.items.findIndex(i => i.value === value && !i.disabled);
|
||||
if (idx !== -1) selectIndex(key, idx);
|
||||
};
|
||||
|
||||
const snapToNearest = key => {
|
||||
const offset = dragOffset[key] ?? 0;
|
||||
const steps = Math.round(-offset / ITEM_HEIGHT);
|
||||
dragOffset[key] = 0;
|
||||
interactionMode[key] = null;
|
||||
if (steps !== 0) selectIndex(key, (activeIndex[key] ?? 0) + steps);
|
||||
};
|
||||
|
||||
const isItemSelected = (key, val) => {
|
||||
if (key === HOUR_12) return convertTo12Hour(selectedTime.hour) === val;
|
||||
if (key === PERIOD) return period.value === val;
|
||||
return selectedTime[key] === val;
|
||||
};
|
||||
|
||||
const isColumnHidden = key => key === PERIOD && !showPeriod.value;
|
||||
|
||||
const onWheelScroll = (key, event) => {
|
||||
event.preventDefault();
|
||||
interactionMode[key] = 'wheel';
|
||||
dragOffset[key] = clampDragOffset(key, (dragOffset[key] ?? 0) - event.deltaY);
|
||||
clearTimeout(snapTimers[key]);
|
||||
snapTimers[key] = setTimeout(() => snapToNearest(key), SNAP_DELAY);
|
||||
};
|
||||
|
||||
const onTouchStart = (key, event) => {
|
||||
interactionMode[key] = 'touch';
|
||||
touchStartY[key] = event.touches[0].clientY;
|
||||
dragOffset[key] = 0;
|
||||
};
|
||||
|
||||
const onTouchMove = (key, event) => {
|
||||
event.preventDefault();
|
||||
dragOffset[key] = clampDragOffset(
|
||||
key,
|
||||
event.touches[0].clientY - touchStartY[key]
|
||||
);
|
||||
};
|
||||
|
||||
const onTouchEnd = key => snapToNearest(key);
|
||||
|
||||
const visibleColumnKeys = computed(() =>
|
||||
columns.value.filter(col => !isColumnHidden(col.key)).map(col => col.key)
|
||||
);
|
||||
|
||||
const onKeyDown = (key, event) => {
|
||||
const { key: pressed, shiftKey } = event;
|
||||
if (pressed === 'ArrowUp' || pressed === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
selectIndex(
|
||||
key,
|
||||
(activeIndex[key] ?? 0) + (pressed === 'ArrowDown' ? 1 : -1)
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (pressed === 'Tab') {
|
||||
const keys = visibleColumnKeys.value;
|
||||
const next = keys.indexOf(key) + (shiftKey ? -1 : 1);
|
||||
if (next >= 0 && next < keys.length) {
|
||||
event.preventDefault();
|
||||
columnRefs[keys[next]]?.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const syncAllColumns = () => {
|
||||
columns.value.forEach(col => {
|
||||
activeIndex[col.key] = getActiveIndex(col.key);
|
||||
});
|
||||
};
|
||||
|
||||
const formatTabs = [
|
||||
{ label: t('DATE_PICKER.FORMAT_24H'), value: H24 },
|
||||
{ label: t('DATE_PICKER.FORMAT_12H'), value: H12 },
|
||||
];
|
||||
const activeFormatTab = computed(() => (is24HourFormat.value ? 0 : 1));
|
||||
|
||||
const onFormatChange = tab => {
|
||||
is24HourFormat.value = tab.value === H24;
|
||||
syncAllColumns();
|
||||
};
|
||||
|
||||
watch(model, v => {
|
||||
selectedTime.hour = v.hour;
|
||||
selectedTime.minute = v.minute;
|
||||
selectedTime.second = v.second ?? 0;
|
||||
syncAllColumns();
|
||||
});
|
||||
|
||||
onMounted(syncAllColumns);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative flex flex-col items-center py-2 w-full">
|
||||
<div class="mb-4">
|
||||
<TabBar
|
||||
:tabs="formatTabs"
|
||||
:initial-active-tab="activeFormatTab"
|
||||
@tab-changed="onFormatChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center z-[1] mb-1">
|
||||
<template v-for="(col, colIdx) in columns" :key="`label-${col.key}`">
|
||||
<span
|
||||
class="text-center text-xs font-medium text-n-slate-11 transition-all duration-300 ease-in-out overflow-hidden"
|
||||
:class="
|
||||
isColumnHidden(col.key) ? 'w-0 opacity-0' : 'w-14 opacity-100'
|
||||
"
|
||||
>
|
||||
{{ col.label }}
|
||||
</span>
|
||||
<span
|
||||
v-if="colIdx < columns.length - 1"
|
||||
class="text-lg font-520 text-transparent transition-all duration-300 ease-in-out overflow-hidden"
|
||||
:class="
|
||||
isColumnHidden(columns[colIdx + 1]?.key)
|
||||
? 'w-0 opacity-0'
|
||||
: 'opacity-100'
|
||||
"
|
||||
>
|
||||
{{ SEPARATOR }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="relative w-full">
|
||||
<div
|
||||
class="absolute inset-x-6 h-10 rounded-2xl bg-n-solid-active outline outline-1 -outline-offset-1 outline-n-weak pointer-events-none shadow-inner"
|
||||
:style="{ top: `${CENTER_ROW * ITEM_HEIGHT}px` }"
|
||||
/>
|
||||
<div class="flex items-center justify-center z-[1] relative">
|
||||
<template v-for="(col, colIdx) in columns" :key="col.key">
|
||||
<div
|
||||
:ref="
|
||||
el => {
|
||||
if (el) columnRefs[col.key] = el;
|
||||
}
|
||||
"
|
||||
:tabindex="isColumnHidden(col.key) ? -1 : 0"
|
||||
class="time-wheel relative overflow-hidden transition-all duration-300 ease-in-out outline-none"
|
||||
:class="
|
||||
isColumnHidden(col.key) ? 'w-0 opacity-0' : 'w-14 opacity-100'
|
||||
"
|
||||
:style="{ height: `${VISIBLE_ROWS * ITEM_HEIGHT}px` }"
|
||||
@wheel.prevent="onWheelScroll(col.key, $event)"
|
||||
@touchstart="onTouchStart(col.key, $event)"
|
||||
@touchmove.prevent="onTouchMove(col.key, $event)"
|
||||
@touchend="onTouchEnd(col.key)"
|
||||
@keydown="onKeyDown(col.key, $event)"
|
||||
@focus="focusedColumn = col.key"
|
||||
@blur="focusedColumn = null"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col items-center"
|
||||
:class="{
|
||||
'transition-transform duration-200 ease-out':
|
||||
!interactionMode[col.key],
|
||||
'transition-transform duration-100 ease-out':
|
||||
interactionMode[col.key] === 'wheel',
|
||||
}"
|
||||
:style="{
|
||||
transform: `translateY(${getColumnTranslateY(col.key)}px)`,
|
||||
}"
|
||||
>
|
||||
<button
|
||||
v-for="item in col.items"
|
||||
:key="item.value"
|
||||
:disabled="item.disabled"
|
||||
tabindex="-1"
|
||||
class="flex items-center justify-center w-14 shrink-0 text-base font-semibold transition-colors outline-none"
|
||||
:style="{ height: `${ITEM_HEIGHT}px` }"
|
||||
:class="[
|
||||
isItemSelected(col.key, item.value)
|
||||
? 'text-n-slate-12'
|
||||
: 'text-n-slate-9',
|
||||
item.disabled
|
||||
? 'opacity-20 cursor-not-allowed'
|
||||
: 'cursor-pointer',
|
||||
isItemSelected(col.key, item.value) &&
|
||||
focusedColumn === col.key
|
||||
? 'outline outline-1 outline-n-brand -outline-offset-4 rounded-xl'
|
||||
: '',
|
||||
]"
|
||||
@click="selectByValue(col.key, item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
v-if="colIdx < columns.length - 1 && col.key !== SECOND"
|
||||
class="text-lg font-520 text-n-slate-11"
|
||||
>
|
||||
{{ SEPARATOR }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.time-wheel {
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
rgba(0, 0, 0, 0.3) 15%,
|
||||
rgba(0, 0, 0, 0.7) 30%,
|
||||
black 40%,
|
||||
black 60%,
|
||||
rgba(0, 0, 0, 0.7) 70%,
|
||||
rgba(0, 0, 0, 0.3) 85%,
|
||||
transparent 100%
|
||||
);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
rgba(0, 0, 0, 0.3) 15%,
|
||||
rgba(0, 0, 0, 0.7) 30%,
|
||||
black 40%,
|
||||
black 60%,
|
||||
rgba(0, 0, 0, 0.7) 70%,
|
||||
rgba(0, 0, 0, 0.3) 85%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,308 @@
|
||||
import {
|
||||
startOfDay,
|
||||
subDays,
|
||||
endOfDay,
|
||||
subMonths,
|
||||
addMonths,
|
||||
subYears,
|
||||
addYears,
|
||||
startOfMonth,
|
||||
isSameMonth,
|
||||
format,
|
||||
startOfWeek,
|
||||
endOfWeek,
|
||||
addWeeks,
|
||||
addDays,
|
||||
eachDayOfInterval,
|
||||
endOfMonth,
|
||||
isSameDay,
|
||||
isWithinInterval,
|
||||
} from 'date-fns';
|
||||
|
||||
// Constants for calendar and date ranges
|
||||
export const calendarWeeks = [
|
||||
{ id: 1, label: 'M' },
|
||||
{ id: 2, label: 'T' },
|
||||
{ id: 3, label: 'W' },
|
||||
{ id: 4, label: 'T' },
|
||||
{ id: 5, label: 'F' },
|
||||
{ id: 6, label: 'S' },
|
||||
{ id: 7, label: 'S' },
|
||||
];
|
||||
|
||||
export const dateRanges = [
|
||||
{ label: 'DATE_PICKER.DATE_RANGE_OPTIONS.LAST_7_DAYS', value: 'last7days' },
|
||||
{ label: 'DATE_PICKER.DATE_RANGE_OPTIONS.LAST_30_DAYS', value: 'last30days' },
|
||||
{
|
||||
label: 'DATE_PICKER.DATE_RANGE_OPTIONS.LAST_3_MONTHS',
|
||||
value: 'last3months',
|
||||
separator: true,
|
||||
},
|
||||
{
|
||||
label: 'DATE_PICKER.DATE_RANGE_OPTIONS.LAST_6_MONTHS',
|
||||
value: 'last6months',
|
||||
},
|
||||
{ label: 'DATE_PICKER.DATE_RANGE_OPTIONS.LAST_YEAR', value: 'lastYear' },
|
||||
{
|
||||
label: 'DATE_PICKER.DATE_RANGE_OPTIONS.THIS_WEEK',
|
||||
value: 'thisWeek',
|
||||
separator: true,
|
||||
},
|
||||
{
|
||||
label: 'DATE_PICKER.DATE_RANGE_OPTIONS.MONTH_TO_DATE',
|
||||
value: 'monthToDate',
|
||||
},
|
||||
{
|
||||
label: 'DATE_PICKER.DATE_RANGE_OPTIONS.CUSTOM_RANGE',
|
||||
value: 'custom',
|
||||
separator: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const DATE_RANGE_TYPES = {
|
||||
LAST_7_DAYS: 'last7days',
|
||||
LAST_30_DAYS: 'last30days',
|
||||
LAST_3_MONTHS: 'last3months',
|
||||
LAST_6_MONTHS: 'last6months',
|
||||
LAST_YEAR: 'lastYear',
|
||||
THIS_WEEK: 'thisWeek',
|
||||
MONTH_TO_DATE: 'monthToDate',
|
||||
CUSTOM_RANGE: 'custom',
|
||||
};
|
||||
|
||||
export const CALENDAR_TYPES = {
|
||||
START_CALENDAR: 'start',
|
||||
END_CALENDAR: 'end',
|
||||
};
|
||||
|
||||
export const CALENDAR_PERIODS = {
|
||||
WEEK: 'week',
|
||||
MONTH: 'month',
|
||||
YEAR: 'year',
|
||||
};
|
||||
|
||||
export const TIME_COLUMNS = {
|
||||
HOUR: 'hour',
|
||||
HOUR_12: 'hour12',
|
||||
MINUTE: 'minute',
|
||||
SECOND: 'second',
|
||||
PERIOD: 'period',
|
||||
};
|
||||
|
||||
export const TIME_PERIODS = {
|
||||
AM: 'AM',
|
||||
PM: 'PM',
|
||||
};
|
||||
|
||||
export const TIME_FORMATS = {
|
||||
H24: '24h',
|
||||
H12: '12h',
|
||||
};
|
||||
|
||||
// Utility functions for date operations
|
||||
export const monthName = currentDate => format(currentDate, 'MMMM');
|
||||
export const yearName = currentDate => format(currentDate, 'yyyy');
|
||||
|
||||
export const getIntlDateFormatForLocale = () => {
|
||||
const year = 2222;
|
||||
const month = 12;
|
||||
const day = 15;
|
||||
const date = new Date(year, month - 1, day);
|
||||
const formattedDate = new Intl.DateTimeFormat(navigator.language).format(
|
||||
date
|
||||
);
|
||||
return formattedDate
|
||||
.replace(`${year}`, 'yyyy')
|
||||
.replace(`${month}`, 'MM')
|
||||
.replace(`${day}`, 'dd');
|
||||
};
|
||||
|
||||
// Utility functions for calendar operations
|
||||
export const chunk = (array, size) =>
|
||||
Array.from({ length: Math.ceil(array.length / size) }, (_, index) =>
|
||||
array.slice(index * size, index * size + size)
|
||||
);
|
||||
|
||||
export const getWeeksForMonth = (date, weekStartsOn = 1) => {
|
||||
const startOfTheMonth = startOfMonth(date);
|
||||
const startOfTheFirstWeek = startOfWeek(startOfTheMonth, { weekStartsOn });
|
||||
const endOfTheLastWeek = addDays(startOfTheFirstWeek, 41); // Covering six weeks to fill the calendar
|
||||
return chunk(
|
||||
eachDayOfInterval({ start: startOfTheFirstWeek, end: endOfTheLastWeek }),
|
||||
7
|
||||
);
|
||||
};
|
||||
|
||||
export const moveCalendarDate = (
|
||||
calendar,
|
||||
startCurrentDate,
|
||||
endCurrentDate,
|
||||
direction,
|
||||
period
|
||||
) => {
|
||||
const adjustFunctions = {
|
||||
month: { prev: subMonths, next: addMonths },
|
||||
year: { prev: subYears, next: addYears },
|
||||
};
|
||||
|
||||
const adjust = adjustFunctions[period][direction];
|
||||
|
||||
if (calendar === 'start') {
|
||||
const newStart = adjust(startCurrentDate, 1);
|
||||
return { start: newStart, end: endCurrentDate };
|
||||
}
|
||||
const newEnd = adjust(endCurrentDate, 1);
|
||||
return { start: startCurrentDate, end: newEnd };
|
||||
};
|
||||
|
||||
// Date comparison functions
|
||||
export const isToday = (currentDate, date) =>
|
||||
date.getDate() === currentDate.getDate() &&
|
||||
date.getMonth() === currentDate.getMonth() &&
|
||||
date.getFullYear() === currentDate.getFullYear();
|
||||
|
||||
export const isCurrentMonth = (day, referenceDate) =>
|
||||
isSameMonth(day, referenceDate);
|
||||
|
||||
export const isLastDayOfMonth = day => {
|
||||
const lastDay = endOfMonth(day);
|
||||
return day.getDate() === lastDay.getDate();
|
||||
};
|
||||
|
||||
export const dayIsInRange = (date, startDate, endDate) => {
|
||||
if (!startDate || !endDate) {
|
||||
return false;
|
||||
}
|
||||
// Normalize dates to ignore time differences
|
||||
let startOfDayStart = startOfDay(startDate);
|
||||
let startOfDayEnd = startOfDay(endDate);
|
||||
// Swap if start is greater than end
|
||||
if (startOfDayStart > startOfDayEnd) {
|
||||
[startOfDayStart, startOfDayEnd] = [startOfDayEnd, startOfDayStart];
|
||||
}
|
||||
// Check if the date is within the interval or is the same as the start date
|
||||
return (
|
||||
isSameDay(date, startOfDayStart) ||
|
||||
isWithinInterval(date, {
|
||||
start: startOfDayStart,
|
||||
end: startOfDayEnd,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// Handling hovering states in date range pickers
|
||||
export const isHoveringDayInRange = (
|
||||
day,
|
||||
startDate,
|
||||
endDate,
|
||||
hoveredEndDate
|
||||
) => {
|
||||
if (endDate && hoveredEndDate && startDate <= hoveredEndDate) {
|
||||
// Ensure the start date is not after the hovered end date
|
||||
return isWithinInterval(day, { start: startDate, end: hoveredEndDate });
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isHoveringNextDayInRange = (
|
||||
day,
|
||||
startDate,
|
||||
endDate,
|
||||
hoveredEndDate
|
||||
) => {
|
||||
if (startDate && !endDate && hoveredEndDate) {
|
||||
// If a start date is selected, and we're hovering (but haven't clicked an end date yet)
|
||||
const nextDay = addDays(day, 1);
|
||||
return isWithinInterval(nextDay, { start: startDate, end: hoveredEndDate });
|
||||
}
|
||||
if (startDate && endDate) {
|
||||
// Normal range checking between selected start and end dates
|
||||
const nextDay = addDays(day, 1);
|
||||
return isWithinInterval(nextDay, { start: startDate, end: endDate });
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Helper func to determine active date ranges based on user selection
|
||||
export const getActiveDateRange = (range, currentDate) => {
|
||||
const ranges = {
|
||||
last7days: () => ({
|
||||
start: startOfDay(subDays(currentDate, 6)),
|
||||
end: endOfDay(currentDate),
|
||||
}),
|
||||
last30days: () => ({
|
||||
start: startOfDay(subDays(currentDate, 29)),
|
||||
end: endOfDay(currentDate),
|
||||
}),
|
||||
last3months: () => ({
|
||||
start: startOfDay(subMonths(currentDate, 3)),
|
||||
end: endOfDay(currentDate),
|
||||
}),
|
||||
last6months: () => ({
|
||||
start: startOfDay(subMonths(currentDate, 6)),
|
||||
end: endOfDay(currentDate),
|
||||
}),
|
||||
lastYear: () => ({
|
||||
start: startOfDay(subMonths(currentDate, 12)),
|
||||
end: endOfDay(currentDate),
|
||||
}),
|
||||
thisWeek: () => ({
|
||||
start: startOfDay(startOfWeek(currentDate, { weekStartsOn: 1 })),
|
||||
end: endOfDay(currentDate),
|
||||
}),
|
||||
monthToDate: () => ({
|
||||
start: startOfDay(startOfMonth(currentDate)),
|
||||
end: endOfDay(currentDate),
|
||||
}),
|
||||
custom: () => ({ start: currentDate, end: currentDate }),
|
||||
};
|
||||
|
||||
return (
|
||||
ranges[range] || (() => ({ start: currentDate, end: currentDate }))
|
||||
)();
|
||||
};
|
||||
|
||||
export const isNavigableRange = rangeType =>
|
||||
rangeType === DATE_RANGE_TYPES.MONTH_TO_DATE ||
|
||||
rangeType === DATE_RANGE_TYPES.THIS_WEEK;
|
||||
|
||||
const WEEK_START = 1; // Monday
|
||||
|
||||
const getWeekRangeAtOffset = (offset, currentDate) => {
|
||||
if (offset === 0) {
|
||||
return {
|
||||
start: startOfDay(startOfWeek(currentDate, { weekStartsOn: WEEK_START })),
|
||||
end: endOfDay(currentDate),
|
||||
};
|
||||
}
|
||||
const targetWeek = addWeeks(currentDate, offset);
|
||||
return {
|
||||
start: startOfDay(startOfWeek(targetWeek, { weekStartsOn: WEEK_START })),
|
||||
end: endOfDay(endOfWeek(targetWeek, { weekStartsOn: WEEK_START })),
|
||||
};
|
||||
};
|
||||
|
||||
const getMonthRangeAtOffset = (offset, currentDate) => {
|
||||
if (offset === 0) {
|
||||
return {
|
||||
start: startOfDay(startOfMonth(currentDate)),
|
||||
end: endOfDay(currentDate),
|
||||
};
|
||||
}
|
||||
const targetMonth = addMonths(currentDate, offset);
|
||||
return {
|
||||
start: startOfDay(startOfMonth(targetMonth)),
|
||||
end: endOfDay(endOfMonth(targetMonth)),
|
||||
};
|
||||
};
|
||||
|
||||
export const getRangeAtOffset = (
|
||||
rangeType,
|
||||
offset,
|
||||
currentDate = new Date()
|
||||
) => {
|
||||
if (rangeType === DATE_RANGE_TYPES.THIS_WEEK) {
|
||||
return getWeekRangeAtOffset(offset, currentDate);
|
||||
}
|
||||
return getMonthRangeAtOffset(offset, currentDate);
|
||||
};
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
import {
|
||||
monthName,
|
||||
yearName,
|
||||
getIntlDateFormatForLocale,
|
||||
getWeeksForMonth,
|
||||
isToday,
|
||||
isCurrentMonth,
|
||||
isLastDayOfMonth,
|
||||
dayIsInRange,
|
||||
getActiveDateRange,
|
||||
isHoveringDayInRange,
|
||||
isHoveringNextDayInRange,
|
||||
moveCalendarDate,
|
||||
chunk,
|
||||
} from '../DatePickerHelper';
|
||||
|
||||
describe('Date formatting functions', () => {
|
||||
const testDate = new Date(2020, 4, 15); // May 15, 2020
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(navigator, 'language', 'get').mockReturnValue('en-US');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('returns the correct month name from a date', () => {
|
||||
expect(monthName(testDate)).toBe('May');
|
||||
});
|
||||
|
||||
it('returns the correct year from a date', () => {
|
||||
expect(yearName(testDate)).toBe('2020');
|
||||
});
|
||||
|
||||
it('returns the correct date format for the current locale en-US', () => {
|
||||
const expected = 'MM/dd/yyyy';
|
||||
expect(getIntlDateFormatForLocale()).toBe(expected);
|
||||
});
|
||||
|
||||
it('returns the correct date format for the current locale en-IN', () => {
|
||||
vi.spyOn(navigator, 'language', 'get').mockReturnValue('en-IN');
|
||||
const expected = 'dd/MM/yyyy';
|
||||
expect(getIntlDateFormatForLocale()).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('chunk', () => {
|
||||
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
|
||||
it('correctly chunks an array into smaller arrays of given size', () => {
|
||||
const expected = [
|
||||
[1, 2, 3],
|
||||
[4, 5, 6],
|
||||
[7, 8, 9],
|
||||
];
|
||||
expect(chunk(array, 3)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('handles arrays that do not divide evenly by the chunk size', () => {
|
||||
const expected = [[1, 2], [3, 4], [5, 6], [7, 8], [9]];
|
||||
expect(chunk(array, 2)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWeeksForMonth', () => {
|
||||
it('returns the correct weeks array for a month starting on Monday', () => {
|
||||
const date = new Date(2020, 3, 1); // April 2020
|
||||
const weeks = getWeeksForMonth(date, 1);
|
||||
expect(weeks.length).toBe(6);
|
||||
expect(weeks[0][0]).toEqual(new Date(2020, 2, 30)); // Check if first day of the first week is correct
|
||||
});
|
||||
});
|
||||
|
||||
describe('moveCalendarDate', () => {
|
||||
it('handles the year transition when moving the start date backward by one month from January', () => {
|
||||
const startDate = new Date(2021, 0, 1);
|
||||
const endDate = new Date(2021, 0, 31);
|
||||
const result = moveCalendarDate(
|
||||
'start',
|
||||
startDate,
|
||||
endDate,
|
||||
'prev',
|
||||
'month'
|
||||
);
|
||||
const expectedStartDate = new Date(2020, 11, 1);
|
||||
expect(result.start.toISOString()).toEqual(expectedStartDate.toISOString());
|
||||
expect(result.end.toISOString()).toEqual(endDate.toISOString());
|
||||
});
|
||||
|
||||
it('handles the year transition when moving the start date forward by one month from January', () => {
|
||||
const startDate = new Date(2020, 0, 1);
|
||||
const endDate = new Date(2020, 1, 31);
|
||||
const result = moveCalendarDate(
|
||||
'start',
|
||||
startDate,
|
||||
endDate,
|
||||
'next',
|
||||
'month'
|
||||
);
|
||||
const expectedStartDate = new Date(2020, 1, 1);
|
||||
expect(result.start.toISOString()).toEqual(expectedStartDate.toISOString());
|
||||
expect(result.end.toISOString()).toEqual(endDate.toISOString());
|
||||
});
|
||||
|
||||
it('handles the year transition when moving the end date forward by one month from December', () => {
|
||||
const startDate = new Date(2021, 11, 1);
|
||||
const endDate = new Date(2021, 11, 31);
|
||||
const result = moveCalendarDate('end', startDate, endDate, 'next', 'month');
|
||||
const expectedEndDate = new Date(2022, 0, 31);
|
||||
expect(result.start).toEqual(startDate);
|
||||
expect(result.end.toISOString()).toEqual(expectedEndDate.toISOString());
|
||||
});
|
||||
it('handles the year transition when moving the end date backward by one month from December', () => {
|
||||
const startDate = new Date(2021, 11, 1);
|
||||
const endDate = new Date(2021, 11, 31);
|
||||
const result = moveCalendarDate('end', startDate, endDate, 'prev', 'month');
|
||||
const expectedEndDate = new Date(2021, 10, 30);
|
||||
|
||||
expect(result.start).toEqual(startDate);
|
||||
expect(result.end.toISOString()).toEqual(expectedEndDate.toISOString());
|
||||
});
|
||||
});
|
||||
|
||||
describe('isToday', () => {
|
||||
it('returns true if the dates are the same', () => {
|
||||
const today = new Date();
|
||||
const alsoToday = new Date(today);
|
||||
expect(isToday(today, alsoToday)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns false if the dates are not the same', () => {
|
||||
const today = new Date();
|
||||
const notToday = new Date(
|
||||
today.getFullYear(),
|
||||
today.getMonth(),
|
||||
today.getDate() - 1
|
||||
);
|
||||
expect(isToday(today, notToday)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCurrentMonth', () => {
|
||||
const referenceDate = new Date(2020, 6, 15); // July 15, 2020
|
||||
|
||||
it('returns true if the day is in the same month as the reference date', () => {
|
||||
const testDay = new Date(2020, 6, 1);
|
||||
expect(isCurrentMonth(testDay, referenceDate)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns false if the day is not in the same month as the reference date', () => {
|
||||
const testDay = new Date(2020, 5, 30);
|
||||
expect(isCurrentMonth(testDay, referenceDate)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLastDayOfMonth', () => {
|
||||
it('returns true if the day is the last day of the month', () => {
|
||||
const testDay = new Date(2020, 6, 31); // July 31, 2020
|
||||
expect(isLastDayOfMonth(testDay)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns false if the day is not the last day of the month', () => {
|
||||
const testDay = new Date(2020, 6, 30); // July 30, 2020
|
||||
expect(isLastDayOfMonth(testDay)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('dayIsInRange', () => {
|
||||
it('returns true if the date is within the range', () => {
|
||||
const start = new Date(2020, 1, 10);
|
||||
const end = new Date(2020, 1, 20);
|
||||
const testDate = new Date(2020, 1, 15);
|
||||
expect(dayIsInRange(testDate, start, end)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns true if the date is the same as the start date', () => {
|
||||
const start = new Date(2020, 1, 10);
|
||||
const end = new Date(2020, 1, 20);
|
||||
const testDate = new Date(2020, 1, 10);
|
||||
expect(dayIsInRange(testDate, start, end)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns false if the date is outside the range', () => {
|
||||
const start = new Date(2020, 1, 10);
|
||||
const end = new Date(2020, 1, 20);
|
||||
const testDate = new Date(2020, 1, 9);
|
||||
expect(dayIsInRange(testDate, start, end)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isHoveringDayInRange', () => {
|
||||
const startDate = new Date(2020, 6, 10);
|
||||
const endDate = new Date(2020, 6, 20);
|
||||
const hoveredEndDate = new Date(2020, 6, 15);
|
||||
|
||||
it('returns true if the day is within the start and hovered end dates', () => {
|
||||
const testDay = new Date(2020, 6, 12);
|
||||
expect(
|
||||
isHoveringDayInRange(testDay, startDate, endDate, hoveredEndDate)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns false if the day is outside the hovered date range', () => {
|
||||
const testDay = new Date(2020, 6, 16);
|
||||
expect(
|
||||
isHoveringDayInRange(testDay, startDate, endDate, hoveredEndDate)
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isHoveringNextDayInRange', () => {
|
||||
const startDate = new Date(2020, 6, 10);
|
||||
const hoveredEndDate = new Date(2020, 6, 15);
|
||||
|
||||
it('returns true if the next day after the given day is within the start and hovered end dates', () => {
|
||||
const testDay = new Date(2020, 6, 14);
|
||||
expect(
|
||||
isHoveringNextDayInRange(testDay, startDate, null, hoveredEndDate)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns false if the next day is outside the start and hovered end dates', () => {
|
||||
const testDay = new Date(2020, 6, 15);
|
||||
expect(
|
||||
isHoveringNextDayInRange(testDay, startDate, null, hoveredEndDate)
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActiveDateRange', () => {
|
||||
const currentDate = new Date(2020, 5, 15, 12, 0); // May 15, 2020, at noon
|
||||
|
||||
beforeEach(() => {
|
||||
// Mocking the current date to ensure consistency in tests
|
||||
vi.useFakeTimers().setSystemTime(currentDate.getTime());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('returns the correct range for "last7days"', () => {
|
||||
const range = getActiveDateRange('last7days', new Date());
|
||||
const expectedStart = new Date(2020, 5, 9);
|
||||
expectedStart.setHours(0, 0, 0, 0);
|
||||
const expectedEnd = new Date();
|
||||
expectedEnd.setHours(23, 59, 59, 999);
|
||||
|
||||
expect(range.start).toEqual(expectedStart);
|
||||
expect(range.end).toEqual(expectedEnd);
|
||||
});
|
||||
|
||||
it('returns the correct range for "last30days"', () => {
|
||||
const range = getActiveDateRange('last30days', new Date());
|
||||
const expectedStart = new Date(2020, 4, 17);
|
||||
expectedStart.setHours(0, 0, 0, 0);
|
||||
const expectedEnd = new Date();
|
||||
expectedEnd.setHours(23, 59, 59, 999);
|
||||
|
||||
expect(range.start).toEqual(expectedStart);
|
||||
expect(range.end).toEqual(expectedEnd);
|
||||
});
|
||||
|
||||
it('returns the correct range for "last3months"', () => {
|
||||
const range = getActiveDateRange('last3months', new Date());
|
||||
const expectedStart = new Date(2020, 2, 15);
|
||||
expectedStart.setHours(0, 0, 0, 0);
|
||||
const expectedEnd = new Date();
|
||||
expectedEnd.setHours(23, 59, 59, 999);
|
||||
|
||||
expect(range.start).toEqual(expectedStart);
|
||||
expect(range.end).toEqual(expectedEnd);
|
||||
});
|
||||
|
||||
it('returns the correct range for "last6months"', () => {
|
||||
const range = getActiveDateRange('last6months', new Date());
|
||||
const expectedStart = new Date(2019, 11, 15);
|
||||
expectedStart.setHours(0, 0, 0, 0);
|
||||
const expectedEnd = new Date();
|
||||
expectedEnd.setHours(23, 59, 59, 999);
|
||||
|
||||
expect(range.start).toEqual(expectedStart);
|
||||
expect(range.end).toEqual(expectedEnd);
|
||||
});
|
||||
|
||||
it('returns the correct range for "lastYear"', () => {
|
||||
const range = getActiveDateRange('lastYear', new Date());
|
||||
const expectedStart = new Date(2019, 5, 15);
|
||||
expectedStart.setHours(0, 0, 0, 0);
|
||||
const expectedEnd = new Date();
|
||||
expectedEnd.setHours(23, 59, 59, 999);
|
||||
|
||||
expect(range.start).toEqual(expectedStart);
|
||||
expect(range.end).toEqual(expectedEnd);
|
||||
});
|
||||
|
||||
it('returns the correct range for "custom date range"', () => {
|
||||
const range = getActiveDateRange('custom', new Date());
|
||||
expect(range.start).toEqual(new Date(currentDate));
|
||||
expect(range.end).toEqual(new Date(currentDate));
|
||||
});
|
||||
|
||||
it('handles an unknown range label gracefully', () => {
|
||||
const range = getActiveDateRange('unknown', new Date());
|
||||
expect(range.start).toEqual(new Date(currentDate));
|
||||
expect(range.end).toEqual(new Date(currentDate));
|
||||
});
|
||||
});
|
||||
@@ -107,7 +107,7 @@ defineExpose({ open, close });
|
||||
<TeleportWithDirection to="body">
|
||||
<dialog
|
||||
ref="dialogRef"
|
||||
class="w-full transition-all duration-300 ease-in-out shadow-xl rounded-xl"
|
||||
class="w-full transition-all duration-300 ease-in-out shadow-xl rounded-xl focus-within:outline-none focus-within:outline-0"
|
||||
:class="[
|
||||
maxWidthClass,
|
||||
positionClass,
|
||||
|
||||
Reference in New Issue
Block a user