673 lines
28 KiB
JavaScript
673 lines
28 KiB
JavaScript
import { getLocalTimeZone, isSameDay, isSameMonth, isToday, } from "@internationalized/date";
|
|
import { attachRef, DOMContext, } from "svelte-toolbelt";
|
|
import { Context, watch } from "runed";
|
|
import { CalendarRootContext } from "../calendar/calendar.svelte.js";
|
|
import { useId } from "../../internal/use-id.js";
|
|
import { boolToStr, boolToEmptyStrOrUndef } from "../../internal/attrs.js";
|
|
import { getAnnouncer } from "../../internal/date-time/announcer.js";
|
|
import { createFormatter } from "../../internal/date-time/formatter.js";
|
|
import { calendarAttrs, createMonths, getCalendarElementProps, getCalendarHeadingValue, getDefaultYears, getIsNextButtonDisabled, getIsPrevButtonDisabled, getWeekdays, handleCalendarKeydown, handleCalendarNextPage, handleCalendarPrevPage, shiftCalendarFocus, useEnsureNonDisabledPlaceholder, useMonthViewOptionsSync, useMonthViewPlaceholderSync, } from "../../internal/date-time/calendar-helpers.svelte.js";
|
|
import { areAllDaysBetweenValid, getDateValueType, isAfter, isBefore, isBetweenInclusive, toDate, } from "../../internal/date-time/utils.js";
|
|
import { onMount, untrack } from "svelte";
|
|
const RangeCalendarCellContext = new Context("RangeCalendar.Cell");
|
|
export class RangeCalendarRootState {
|
|
static create(opts) {
|
|
return CalendarRootContext.set(new RangeCalendarRootState(opts));
|
|
}
|
|
opts;
|
|
attachment;
|
|
visibleMonths = $derived.by(() => this.months.map((month) => month.value));
|
|
months = $state([]);
|
|
announcer;
|
|
formatter;
|
|
accessibleHeadingId = useId();
|
|
focusedValue = $state(undefined);
|
|
lastPressedDateValue = undefined;
|
|
domContext;
|
|
/**
|
|
* This derived state holds an array of localized day names for the current
|
|
* locale and calendar view. It dynamically syncs with the 'weekStartsOn' option,
|
|
* updating its content when the option changes. Using this state to render the
|
|
* calendar's days of the week is strongly recommended, as it guarantees that
|
|
* the days are correctly formatted for the current locale and calendar view.
|
|
*/
|
|
weekdays = $derived.by(() => {
|
|
return getWeekdays({
|
|
months: this.months,
|
|
formatter: this.formatter,
|
|
weekdayFormat: this.opts.weekdayFormat.current,
|
|
});
|
|
});
|
|
isStartInvalid = $derived.by(() => {
|
|
if (!this.opts.startValue.current)
|
|
return false;
|
|
return (this.isDateUnavailable(this.opts.startValue.current) ||
|
|
this.isDateDisabled(this.opts.startValue.current));
|
|
});
|
|
isEndInvalid = $derived.by(() => {
|
|
if (!this.opts.endValue.current)
|
|
return false;
|
|
return (this.isDateUnavailable(this.opts.endValue.current) ||
|
|
this.isDateDisabled(this.opts.endValue.current));
|
|
});
|
|
isInvalid = $derived.by(() => {
|
|
if (this.isStartInvalid || this.isEndInvalid)
|
|
return true;
|
|
if (this.opts.endValue.current &&
|
|
this.opts.startValue.current &&
|
|
isBefore(this.opts.endValue.current, this.opts.startValue.current))
|
|
return true;
|
|
return false;
|
|
});
|
|
isNextButtonDisabled = $derived.by(() => {
|
|
return getIsNextButtonDisabled({
|
|
maxValue: this.opts.maxValue.current,
|
|
months: this.months,
|
|
disabled: this.opts.disabled.current,
|
|
});
|
|
});
|
|
isPrevButtonDisabled = $derived.by(() => {
|
|
return getIsPrevButtonDisabled({
|
|
minValue: this.opts.minValue.current,
|
|
months: this.months,
|
|
disabled: this.opts.disabled.current,
|
|
});
|
|
});
|
|
headingValue = $derived.by(() => {
|
|
this.opts.monthFormat.current;
|
|
this.opts.yearFormat.current;
|
|
return getCalendarHeadingValue({
|
|
months: this.months,
|
|
formatter: this.formatter,
|
|
locale: this.opts.locale.current,
|
|
});
|
|
});
|
|
fullCalendarLabel = $derived.by(() => `${this.opts.calendarLabel.current} ${this.headingValue}`);
|
|
highlightedRange = $derived.by(() => {
|
|
if (this.opts.startValue.current && this.opts.endValue.current)
|
|
return null;
|
|
if (!this.opts.startValue.current || !this.focusedValue)
|
|
return null;
|
|
const isStartBeforeFocused = isBefore(this.opts.startValue.current, this.focusedValue);
|
|
const start = isStartBeforeFocused ? this.opts.startValue.current : this.focusedValue;
|
|
const end = isStartBeforeFocused ? this.focusedValue : this.opts.startValue.current;
|
|
const range = { start, end };
|
|
if (isSameDay(start.add({ days: 1 }), end) || isSameDay(start, end)) {
|
|
return range;
|
|
}
|
|
const isValid = areAllDaysBetweenValid(start, end, this.isDateUnavailable, this.isDateDisabled);
|
|
if (isValid)
|
|
return range;
|
|
return null;
|
|
});
|
|
initialPlaceholderYear = $derived.by(() => untrack(() => this.opts.placeholder.current.year));
|
|
defaultYears = $derived.by(() => {
|
|
return getDefaultYears({
|
|
minValue: this.opts.minValue.current,
|
|
maxValue: this.opts.maxValue.current,
|
|
placeholderYear: this.initialPlaceholderYear,
|
|
});
|
|
});
|
|
constructor(opts) {
|
|
this.opts = opts;
|
|
this.attachment = attachRef(opts.ref);
|
|
this.domContext = new DOMContext(opts.ref);
|
|
this.announcer = getAnnouncer(null);
|
|
this.formatter = createFormatter({
|
|
initialLocale: this.opts.locale.current,
|
|
monthFormat: this.opts.monthFormat,
|
|
yearFormat: this.opts.yearFormat,
|
|
});
|
|
this.months = createMonths({
|
|
dateObj: this.opts.placeholder.current,
|
|
weekStartsOn: this.opts.weekStartsOn.current,
|
|
locale: this.opts.locale.current,
|
|
fixedWeeks: this.opts.fixedWeeks.current,
|
|
numberOfMonths: this.opts.numberOfMonths.current,
|
|
});
|
|
$effect.pre(() => {
|
|
if (this.formatter.getLocale() === this.opts.locale.current)
|
|
return;
|
|
this.formatter.setLocale(this.opts.locale.current);
|
|
});
|
|
onMount(() => {
|
|
this.announcer = getAnnouncer(this.domContext.getDocument());
|
|
});
|
|
/**
|
|
* Updates the displayed months based on changes in the placeholder values,
|
|
* which determines the month to show in the calendar.
|
|
*/
|
|
useMonthViewPlaceholderSync({
|
|
placeholder: this.opts.placeholder,
|
|
getVisibleMonths: () => this.visibleMonths,
|
|
weekStartsOn: this.opts.weekStartsOn,
|
|
locale: this.opts.locale,
|
|
fixedWeeks: this.opts.fixedWeeks,
|
|
numberOfMonths: this.opts.numberOfMonths,
|
|
setMonths: this.setMonths,
|
|
});
|
|
/**
|
|
* Updates the displayed months based on changes in the options values,
|
|
* which determines the month to show in the calendar.
|
|
*/
|
|
useMonthViewOptionsSync({
|
|
fixedWeeks: this.opts.fixedWeeks,
|
|
locale: this.opts.locale,
|
|
numberOfMonths: this.opts.numberOfMonths,
|
|
placeholder: this.opts.placeholder,
|
|
setMonths: this.setMonths,
|
|
weekStartsOn: this.opts.weekStartsOn,
|
|
});
|
|
/**
|
|
* Update the accessible heading's text content when the `fullCalendarLabel`
|
|
* changes.
|
|
*/
|
|
$effect(() => {
|
|
const node = this.domContext.getElementById(this.accessibleHeadingId);
|
|
if (!node)
|
|
return;
|
|
node.textContent = this.fullCalendarLabel;
|
|
});
|
|
/**
|
|
* Synchronize the start and end values with the `value` in case
|
|
* it is updated externally.
|
|
*/
|
|
watch(() => this.opts.value.current, (value) => {
|
|
if (value.start && value.end) {
|
|
this.opts.startValue.current = value.start;
|
|
this.opts.endValue.current = value.end;
|
|
}
|
|
else if (value.start) {
|
|
this.opts.startValue.current = value.start;
|
|
this.opts.endValue.current = undefined;
|
|
}
|
|
else if (value.start === undefined && value.end === undefined) {
|
|
this.opts.startValue.current = undefined;
|
|
this.opts.endValue.current = undefined;
|
|
}
|
|
});
|
|
/**
|
|
* Synchronize the placeholder value with the current start value
|
|
*/
|
|
watch(() => this.opts.value.current, (value) => {
|
|
const startValue = value.start;
|
|
if (startValue && this.opts.placeholder.current !== startValue) {
|
|
this.opts.placeholder.current = startValue;
|
|
}
|
|
});
|
|
/**
|
|
* Check for disabled dates in the selected range when excludeDisabled is enabled
|
|
*/
|
|
watch([
|
|
() => this.opts.startValue.current,
|
|
() => this.opts.endValue.current,
|
|
() => this.opts.excludeDisabled.current,
|
|
], ([startValue, endValue, excludeDisabled]) => {
|
|
if (!excludeDisabled || !startValue || !endValue)
|
|
return;
|
|
if (this.#hasDisabledDatesInRange(startValue, endValue)) {
|
|
this.#setStartValue(undefined);
|
|
this.#setEndValue(undefined);
|
|
this.#announceEmpty();
|
|
}
|
|
});
|
|
watch([() => this.opts.startValue.current, () => this.opts.endValue.current], ([startValue, endValue]) => {
|
|
if (this.opts.value.current &&
|
|
this.opts.value.current.start === startValue &&
|
|
this.opts.value.current.end === endValue) {
|
|
return;
|
|
}
|
|
if (startValue && endValue) {
|
|
this.#updateValue((prev) => {
|
|
if (prev.start === startValue && prev.end === endValue) {
|
|
return prev;
|
|
}
|
|
if (isBefore(endValue, startValue)) {
|
|
const start = startValue;
|
|
const end = endValue;
|
|
this.#setStartValue(end);
|
|
this.#setEndValue(start);
|
|
if (!this.#isRangeValid(endValue, startValue)) {
|
|
this.#setStartValue(startValue);
|
|
this.#setEndValue(undefined);
|
|
return { start: startValue, end: undefined };
|
|
}
|
|
return { start: endValue, end: startValue };
|
|
}
|
|
else {
|
|
if (!this.#isRangeValid(startValue, endValue)) {
|
|
this.#setStartValue(endValue);
|
|
this.#setEndValue(undefined);
|
|
return { start: endValue, end: undefined };
|
|
}
|
|
return {
|
|
start: startValue,
|
|
end: endValue,
|
|
};
|
|
}
|
|
});
|
|
}
|
|
else if (this.opts.value.current &&
|
|
this.opts.value.current.start &&
|
|
this.opts.value.current.end) {
|
|
this.opts.value.current.start = undefined;
|
|
this.opts.value.current.end = undefined;
|
|
}
|
|
});
|
|
this.shiftFocus = this.shiftFocus.bind(this);
|
|
this.handleCellClick = this.handleCellClick.bind(this);
|
|
this.onkeydown = this.onkeydown.bind(this);
|
|
this.nextPage = this.nextPage.bind(this);
|
|
this.prevPage = this.prevPage.bind(this);
|
|
this.nextYear = this.nextYear.bind(this);
|
|
this.prevYear = this.prevYear.bind(this);
|
|
this.setYear = this.setYear.bind(this);
|
|
this.setMonth = this.setMonth.bind(this);
|
|
this.isDateDisabled = this.isDateDisabled.bind(this);
|
|
this.isDateUnavailable = this.isDateUnavailable.bind(this);
|
|
this.isOutsideVisibleMonths = this.isOutsideVisibleMonths.bind(this);
|
|
this.isSelected = this.isSelected.bind(this);
|
|
useEnsureNonDisabledPlaceholder({
|
|
placeholder: opts.placeholder,
|
|
defaultPlaceholder: opts.defaultPlaceholder,
|
|
isDateDisabled: opts.isDateDisabled,
|
|
maxValue: opts.maxValue,
|
|
minValue: opts.minValue,
|
|
ref: opts.ref,
|
|
});
|
|
}
|
|
#updateValue(cb) {
|
|
const value = this.opts.value.current;
|
|
const newValue = cb(value);
|
|
this.opts.value.current = newValue;
|
|
if (newValue.start && newValue.end) {
|
|
this.opts.onRangeSelect?.current?.();
|
|
}
|
|
}
|
|
#setStartValue(value) {
|
|
this.opts.startValue.current = value;
|
|
// update the main value prop immediately for external consumers
|
|
this.#updateValue((prev) => ({
|
|
...prev,
|
|
start: value,
|
|
}));
|
|
}
|
|
#setEndValue(value) {
|
|
this.opts.endValue.current = value;
|
|
// update the main value prop immediately for external consumers
|
|
this.#updateValue((prev) => ({
|
|
...prev,
|
|
end: value,
|
|
}));
|
|
}
|
|
setMonths = (months) => {
|
|
this.months = months;
|
|
};
|
|
isOutsideVisibleMonths(date) {
|
|
return !this.visibleMonths.some((month) => isSameMonth(date, month));
|
|
}
|
|
isDateDisabled(date) {
|
|
if (this.opts.isDateDisabled.current(date) || this.opts.disabled.current)
|
|
return true;
|
|
const minValue = this.opts.minValue.current;
|
|
const maxValue = this.opts.maxValue.current;
|
|
if (minValue && isBefore(date, minValue))
|
|
return true;
|
|
if (maxValue && isAfter(date, maxValue))
|
|
return true;
|
|
return false;
|
|
}
|
|
isDateUnavailable(date) {
|
|
if (this.opts.isDateUnavailable.current(date))
|
|
return true;
|
|
return false;
|
|
}
|
|
isSelectionStart(date) {
|
|
if (!this.opts.startValue.current)
|
|
return false;
|
|
return isSameDay(date, this.opts.startValue.current);
|
|
}
|
|
isSelectionEnd(date) {
|
|
if (!this.opts.endValue.current)
|
|
return false;
|
|
return isSameDay(date, this.opts.endValue.current);
|
|
}
|
|
isSelected(date) {
|
|
if (this.opts.startValue.current && isSameDay(this.opts.startValue.current, date))
|
|
return true;
|
|
if (this.opts.endValue.current && isSameDay(this.opts.endValue.current, date))
|
|
return true;
|
|
if (this.opts.startValue.current && this.opts.endValue.current) {
|
|
return isBetweenInclusive(date, this.opts.startValue.current, this.opts.endValue.current);
|
|
}
|
|
return false;
|
|
}
|
|
#isRangeValid(start, end) {
|
|
// ensure we always use the correct order for calculation
|
|
const orderedStart = isBefore(end, start) ? end : start;
|
|
const orderedEnd = isBefore(end, start) ? start : end;
|
|
const startDate = orderedStart.toDate(getLocalTimeZone());
|
|
const endDate = orderedEnd.toDate(getLocalTimeZone());
|
|
const timeDifference = endDate.getTime() - startDate.getTime();
|
|
const daysDifference = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
|
|
const daysInRange = daysDifference + 1; // +1 to include both start and end days
|
|
if (this.opts.minDays.current && daysInRange < this.opts.minDays.current)
|
|
return false;
|
|
if (this.opts.maxDays.current && daysInRange > this.opts.maxDays.current)
|
|
return false;
|
|
// check for disabled dates in range if excludeDisabled is enabled
|
|
if (this.opts.excludeDisabled.current &&
|
|
this.#hasDisabledDatesInRange(orderedStart, orderedEnd)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
shiftFocus(node, add) {
|
|
return shiftCalendarFocus({
|
|
node,
|
|
add,
|
|
placeholder: this.opts.placeholder,
|
|
calendarNode: this.opts.ref.current,
|
|
isPrevButtonDisabled: this.isPrevButtonDisabled,
|
|
isNextButtonDisabled: this.isNextButtonDisabled,
|
|
months: this.months,
|
|
numberOfMonths: this.opts.numberOfMonths.current,
|
|
});
|
|
}
|
|
#announceEmpty() {
|
|
this.announcer.announce("Selected date is now empty.", "polite");
|
|
}
|
|
#announceSelectedDate(date) {
|
|
this.announcer.announce(`Selected Date: ${this.formatter.selectedDate(date, false)}`, "polite");
|
|
}
|
|
#announceSelectedRange(start, end) {
|
|
this.announcer.announce(`Selected Dates: ${this.formatter.selectedDate(start, false)} to ${this.formatter.selectedDate(end, false)}`, "polite");
|
|
}
|
|
handleCellClick(e, date) {
|
|
if (this.isDateDisabled(date) || this.isDateUnavailable(date))
|
|
return;
|
|
const prevLastPressedDate = this.lastPressedDateValue;
|
|
this.lastPressedDateValue = date;
|
|
if (this.opts.startValue.current && this.highlightedRange === null) {
|
|
if (isSameDay(this.opts.startValue.current, date) &&
|
|
!this.opts.preventDeselect.current &&
|
|
!this.opts.endValue.current) {
|
|
this.#setStartValue(undefined);
|
|
this.opts.placeholder.current = date;
|
|
this.#announceEmpty();
|
|
return;
|
|
}
|
|
else if (!this.opts.endValue.current) {
|
|
e.preventDefault();
|
|
if (prevLastPressedDate && isSameDay(prevLastPressedDate, date)) {
|
|
this.#setStartValue(date);
|
|
this.#announceSelectedDate(date);
|
|
}
|
|
}
|
|
}
|
|
if (this.opts.startValue.current &&
|
|
this.opts.endValue.current &&
|
|
isSameDay(this.opts.endValue.current, date) &&
|
|
!this.opts.preventDeselect.current) {
|
|
this.#setStartValue(undefined);
|
|
this.#setEndValue(undefined);
|
|
this.opts.placeholder.current = date;
|
|
this.#announceEmpty();
|
|
return;
|
|
}
|
|
if (!this.opts.startValue.current) {
|
|
this.#announceSelectedDate(date);
|
|
this.#setStartValue(date);
|
|
}
|
|
else if (!this.opts.endValue.current) {
|
|
// determine the start and end dates for validation
|
|
const startDate = this.opts.startValue.current;
|
|
const endDate = date;
|
|
const orderedStart = isBefore(endDate, startDate) ? endDate : startDate;
|
|
const orderedEnd = isBefore(endDate, startDate) ? startDate : endDate;
|
|
// check if the range violates constraints
|
|
if (!this.#isRangeValid(orderedStart, orderedEnd)) {
|
|
// reset to just the clicked date
|
|
this.#setStartValue(date);
|
|
this.#setEndValue(undefined);
|
|
this.#announceSelectedDate(date);
|
|
}
|
|
else {
|
|
// ensure start and end are properly ordered
|
|
if (isBefore(endDate, startDate)) {
|
|
// backward selection - reorder the values
|
|
this.#setStartValue(endDate);
|
|
this.#setEndValue(startDate);
|
|
this.#announceSelectedRange(endDate, startDate);
|
|
}
|
|
else {
|
|
// forward selection - keep original order
|
|
this.#setEndValue(date);
|
|
this.#announceSelectedRange(this.opts.startValue.current, date);
|
|
}
|
|
}
|
|
}
|
|
else if (this.opts.endValue.current && this.opts.startValue.current) {
|
|
this.#setEndValue(undefined);
|
|
this.#announceSelectedDate(date);
|
|
this.#setStartValue(date);
|
|
}
|
|
}
|
|
onkeydown(event) {
|
|
return handleCalendarKeydown({
|
|
event,
|
|
handleCellClick: this.handleCellClick,
|
|
placeholderValue: this.opts.placeholder.current,
|
|
shiftFocus: this.shiftFocus,
|
|
});
|
|
}
|
|
/**
|
|
* Navigates to the next page of the calendar.
|
|
*/
|
|
nextPage() {
|
|
handleCalendarNextPage({
|
|
fixedWeeks: this.opts.fixedWeeks.current,
|
|
locale: this.opts.locale.current,
|
|
numberOfMonths: this.opts.numberOfMonths.current,
|
|
pagedNavigation: this.opts.pagedNavigation.current,
|
|
setMonths: this.setMonths,
|
|
setPlaceholder: (date) => (this.opts.placeholder.current = date),
|
|
weekStartsOn: this.opts.weekStartsOn.current,
|
|
months: this.months,
|
|
});
|
|
}
|
|
/**
|
|
* Navigates to the previous page of the calendar.
|
|
*/
|
|
prevPage() {
|
|
handleCalendarPrevPage({
|
|
fixedWeeks: this.opts.fixedWeeks.current,
|
|
locale: this.opts.locale.current,
|
|
numberOfMonths: this.opts.numberOfMonths.current,
|
|
pagedNavigation: this.opts.pagedNavigation.current,
|
|
setMonths: this.setMonths,
|
|
setPlaceholder: (date) => (this.opts.placeholder.current = date),
|
|
weekStartsOn: this.opts.weekStartsOn.current,
|
|
months: this.months,
|
|
});
|
|
}
|
|
nextYear() {
|
|
this.opts.placeholder.current = this.opts.placeholder.current.add({ years: 1 });
|
|
}
|
|
prevYear() {
|
|
this.opts.placeholder.current = this.opts.placeholder.current.subtract({ years: 1 });
|
|
}
|
|
setYear(year) {
|
|
this.opts.placeholder.current = this.opts.placeholder.current.set({ year });
|
|
}
|
|
setMonth(month) {
|
|
this.opts.placeholder.current = this.opts.placeholder.current.set({ month });
|
|
}
|
|
getBitsAttr = (part) => {
|
|
return calendarAttrs.getAttr(part, "range-calendar");
|
|
};
|
|
snippetProps = $derived.by(() => ({
|
|
months: this.months,
|
|
weekdays: this.weekdays,
|
|
}));
|
|
props = $derived.by(() => ({
|
|
...getCalendarElementProps({
|
|
fullCalendarLabel: this.fullCalendarLabel,
|
|
id: this.opts.id.current,
|
|
isInvalid: this.isInvalid,
|
|
disabled: this.opts.disabled.current,
|
|
readonly: this.opts.readonly.current,
|
|
}),
|
|
[this.getBitsAttr("root")]: "",
|
|
//
|
|
onkeydown: this.onkeydown,
|
|
...this.attachment,
|
|
}));
|
|
#hasDisabledDatesInRange(start, end) {
|
|
for (let date = start; isBefore(date, end) || isSameDay(date, end); date = date.add({ days: 1 })) {
|
|
if (this.isDateDisabled(date))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
export class RangeCalendarCellState {
|
|
static create(opts) {
|
|
return RangeCalendarCellContext.set(new RangeCalendarCellState(opts, CalendarRootContext.get()));
|
|
}
|
|
opts;
|
|
root;
|
|
attachment;
|
|
cellDate = $derived.by(() => toDate(this.opts.date.current));
|
|
isOutsideMonth = $derived.by(() => !isSameMonth(this.opts.date.current, this.opts.month.current));
|
|
isDisabled = $derived.by(() => this.root.isDateDisabled(this.opts.date.current) ||
|
|
(this.isOutsideMonth && this.root.opts.disableDaysOutsideMonth.current));
|
|
isUnavailable = $derived.by(() => this.root.opts.isDateUnavailable.current(this.opts.date.current));
|
|
isDateToday = $derived.by(() => isToday(this.opts.date.current, getLocalTimeZone()));
|
|
isOutsideVisibleMonths = $derived.by(() => this.root.isOutsideVisibleMonths(this.opts.date.current));
|
|
isFocusedDate = $derived.by(() => isSameDay(this.opts.date.current, this.root.opts.placeholder.current));
|
|
isSelectedDate = $derived.by(() => this.root.isSelected(this.opts.date.current));
|
|
isSelectionStart = $derived.by(() => this.root.isSelectionStart(this.opts.date.current));
|
|
isRangeStart = $derived.by(() => this.root.isSelectionStart(this.opts.date.current));
|
|
isRangeEnd = $derived.by(() => {
|
|
if (!this.root.opts.endValue.current)
|
|
return this.root.isSelectionStart(this.opts.date.current);
|
|
return this.root.isSelectionEnd(this.opts.date.current);
|
|
});
|
|
isRangeMiddle = $derived.by(() => this.isSelectionMiddle);
|
|
isSelectionMiddle = $derived.by(() => {
|
|
return this.isSelectedDate && !this.isSelectionStart && !this.isSelectionEnd;
|
|
});
|
|
isSelectionEnd = $derived.by(() => this.root.isSelectionEnd(this.opts.date.current));
|
|
isHighlighted = $derived.by(() => this.root.highlightedRange
|
|
? isBetweenInclusive(this.opts.date.current, this.root.highlightedRange.start, this.root.highlightedRange.end)
|
|
: false);
|
|
labelText = $derived.by(() => this.root.formatter.custom(this.cellDate, {
|
|
weekday: "long",
|
|
month: "long",
|
|
day: "numeric",
|
|
year: "numeric",
|
|
}));
|
|
constructor(opts, root) {
|
|
this.opts = opts;
|
|
this.root = root;
|
|
this.attachment = attachRef(opts.ref);
|
|
}
|
|
snippetProps = $derived.by(() => ({
|
|
disabled: this.isDisabled,
|
|
unavailable: this.isUnavailable,
|
|
selected: this.isSelectedDate,
|
|
}));
|
|
ariaDisabled = $derived.by(() => {
|
|
return (this.isDisabled ||
|
|
(this.isOutsideMonth && this.root.opts.disableDaysOutsideMonth.current) ||
|
|
this.isUnavailable);
|
|
});
|
|
sharedDataAttrs = $derived.by(() => ({
|
|
"data-unavailable": boolToEmptyStrOrUndef(this.isUnavailable),
|
|
"data-today": this.isDateToday ? "" : undefined,
|
|
"data-outside-month": this.isOutsideMonth ? "" : undefined,
|
|
"data-outside-visible-months": this.isOutsideVisibleMonths ? "" : undefined,
|
|
"data-focused": this.isFocusedDate ? "" : undefined,
|
|
"data-selection-start": this.isSelectionStart ? "" : undefined,
|
|
"data-selection-end": this.isSelectionEnd ? "" : undefined,
|
|
"data-range-start": this.isRangeStart ? "" : undefined,
|
|
"data-range-end": this.isRangeEnd ? "" : undefined,
|
|
"data-range-middle": this.isRangeMiddle ? "" : undefined,
|
|
"data-highlighted": this.isHighlighted ? "" : undefined,
|
|
"data-selected": boolToEmptyStrOrUndef(this.isSelectedDate),
|
|
"data-value": this.opts.date.current.toString(),
|
|
"data-type": getDateValueType(this.opts.date.current),
|
|
"data-disabled": boolToEmptyStrOrUndef(this.isDisabled ||
|
|
(this.isOutsideMonth && this.root.opts.disableDaysOutsideMonth.current)),
|
|
}));
|
|
props = $derived.by(() => ({
|
|
id: this.opts.id.current,
|
|
role: "gridcell",
|
|
"aria-selected": boolToStr(this.isSelectedDate),
|
|
"aria-disabled": boolToStr(this.ariaDisabled),
|
|
...this.sharedDataAttrs,
|
|
[this.root.getBitsAttr("cell")]: "",
|
|
...this.attachment,
|
|
}));
|
|
}
|
|
export class RangeCalendarDayState {
|
|
static create(opts) {
|
|
return new RangeCalendarDayState(opts, RangeCalendarCellContext.get());
|
|
}
|
|
opts;
|
|
cell;
|
|
attachment;
|
|
constructor(opts, cell) {
|
|
this.opts = opts;
|
|
this.cell = cell;
|
|
this.attachment = attachRef(opts.ref);
|
|
this.onclick = this.onclick.bind(this);
|
|
this.onmouseenter = this.onmouseenter.bind(this);
|
|
this.onfocusin = this.onfocusin.bind(this);
|
|
}
|
|
#tabindex = $derived.by(() => (this.cell.isOutsideMonth && this.cell.root.opts.disableDaysOutsideMonth.current) ||
|
|
this.cell.isDisabled
|
|
? undefined
|
|
: this.cell.isFocusedDate
|
|
? 0
|
|
: -1);
|
|
onclick(e) {
|
|
if (this.cell.isDisabled)
|
|
return;
|
|
this.cell.root.handleCellClick(e, this.cell.opts.date.current);
|
|
}
|
|
onmouseenter(_) {
|
|
if (this.cell.isDisabled)
|
|
return;
|
|
this.cell.root.focusedValue = this.cell.opts.date.current;
|
|
}
|
|
onfocusin(_) {
|
|
if (this.cell.isDisabled)
|
|
return;
|
|
this.cell.root.focusedValue = this.cell.opts.date.current;
|
|
}
|
|
snippetProps = $derived.by(() => ({
|
|
disabled: this.cell.isDisabled,
|
|
unavailable: this.cell.isUnavailable,
|
|
selected: this.cell.isSelectedDate,
|
|
day: `${this.cell.opts.date.current.day}`,
|
|
}));
|
|
props = $derived.by(() => ({
|
|
id: this.opts.id.current,
|
|
role: "button",
|
|
"aria-label": this.cell.labelText,
|
|
"aria-disabled": boolToStr(this.cell.ariaDisabled),
|
|
...this.cell.sharedDataAttrs,
|
|
tabindex: this.#tabindex,
|
|
[this.cell.root.getBitsAttr("day")]: "",
|
|
// Shared logic for range calendar and calendar
|
|
"data-bits-day": "",
|
|
//
|
|
onclick: this.onclick,
|
|
onmouseenter: this.onmouseenter,
|
|
onfocusin: this.onfocusin,
|
|
...this.attachment,
|
|
}));
|
|
}
|