This commit is contained in:
+870
@@ -0,0 +1,870 @@
|
||||
import { getLocalTimeZone, isSameDay, isSameMonth, isToday, } from "@internationalized/date";
|
||||
import { DEV } from "esm-env";
|
||||
import { onMount, untrack } from "svelte";
|
||||
import { attachRef, DOMContext, } from "svelte-toolbelt";
|
||||
import { Context, watch } from "runed";
|
||||
import { boolToStr, boolToStrTrueOrUndef, boolToEmptyStrOrUndef } from "../../internal/attrs.js";
|
||||
import { useId } from "../../internal/use-id.js";
|
||||
import { getAnnouncer } from "../../internal/date-time/announcer.js";
|
||||
import { createFormatter } from "../../internal/date-time/formatter.js";
|
||||
import { calendarAttrs, createAccessibleHeading, createMonths, getCalendarElementProps, getCalendarHeadingValue, getDateWithPreviousTime, getDefaultYears, getIsNextButtonDisabled, getIsPrevButtonDisabled, getWeekdays, handleCalendarKeydown, handleCalendarNextPage, handleCalendarPrevPage, shiftCalendarFocus, useEnsureNonDisabledPlaceholder, useMonthViewOptionsSync, useMonthViewPlaceholderSync, } from "../../internal/date-time/calendar-helpers.svelte.js";
|
||||
import { getDateValueType, isBefore, toDate } from "../../internal/date-time/utils.js";
|
||||
export const CalendarRootContext = new Context("Calendar.Root | RangeCalender.Root");
|
||||
export class CalendarRootState {
|
||||
static create(opts) {
|
||||
return CalendarRootContext.set(new CalendarRootState(opts));
|
||||
}
|
||||
opts;
|
||||
visibleMonths = $derived.by(() => this.months.map((month) => month.value));
|
||||
formatter;
|
||||
accessibleHeadingId = useId();
|
||||
domContext;
|
||||
attachment;
|
||||
months = $state([]);
|
||||
announcer;
|
||||
constructor(opts) {
|
||||
this.opts = opts;
|
||||
this.attachment = attachRef(this.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.setMonths = this.setMonths.bind(this);
|
||||
this.nextPage = this.nextPage.bind(this);
|
||||
this.prevPage = this.prevPage.bind(this);
|
||||
this.prevYear = this.prevYear.bind(this);
|
||||
this.nextYear = this.nextYear.bind(this);
|
||||
this.setYear = this.setYear.bind(this);
|
||||
this.setMonth = this.setMonth.bind(this);
|
||||
this.isOutsideVisibleMonths = this.isOutsideVisibleMonths.bind(this);
|
||||
this.isDateDisabled = this.isDateDisabled.bind(this);
|
||||
this.isDateSelected = this.isDateSelected.bind(this);
|
||||
this.shiftFocus = this.shiftFocus.bind(this);
|
||||
this.handleCellClick = this.handleCellClick.bind(this);
|
||||
this.handleMultipleUpdate = this.handleMultipleUpdate.bind(this);
|
||||
this.handleSingleUpdate = this.handleSingleUpdate.bind(this);
|
||||
this.onkeydown = this.onkeydown.bind(this);
|
||||
this.getBitsAttr = this.getBitsAttr.bind(this);
|
||||
onMount(() => {
|
||||
this.announcer = getAnnouncer(this.domContext.getDocument());
|
||||
});
|
||||
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,
|
||||
});
|
||||
this.#setupInitialFocusEffect();
|
||||
this.#setupAccessibleHeadingEffect();
|
||||
this.#setupFormatterEffect();
|
||||
/**
|
||||
* Updates the displayed months based on changes in the placeholder value.
|
||||
*/
|
||||
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: (months) => (this.months = months),
|
||||
});
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
watch(() => this.fullCalendarLabel, (label) => {
|
||||
const node = this.domContext.getElementById(this.accessibleHeadingId);
|
||||
if (!node)
|
||||
return;
|
||||
node.textContent = label;
|
||||
});
|
||||
/**
|
||||
* Synchronize the placeholder value with the current value.
|
||||
*/
|
||||
watch(() => this.opts.value.current, () => {
|
||||
const value = this.opts.value.current;
|
||||
if (Array.isArray(value) && value.length) {
|
||||
const lastValue = value[value.length - 1];
|
||||
if (lastValue && this.opts.placeholder.current !== lastValue) {
|
||||
this.opts.placeholder.current = lastValue;
|
||||
}
|
||||
}
|
||||
else if (!Array.isArray(value) &&
|
||||
value &&
|
||||
this.opts.placeholder.current !== value) {
|
||||
this.opts.placeholder.current = value;
|
||||
}
|
||||
});
|
||||
useEnsureNonDisabledPlaceholder({
|
||||
placeholder: opts.placeholder,
|
||||
defaultPlaceholder: opts.defaultPlaceholder,
|
||||
isDateDisabled: opts.isDateDisabled,
|
||||
maxValue: opts.maxValue,
|
||||
minValue: opts.minValue,
|
||||
ref: opts.ref,
|
||||
});
|
||||
}
|
||||
setMonths(months) {
|
||||
this.months = months;
|
||||
}
|
||||
/**
|
||||
* 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,
|
||||
});
|
||||
});
|
||||
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,
|
||||
});
|
||||
});
|
||||
#setupInitialFocusEffect() {
|
||||
$effect(() => {
|
||||
const initialFocus = untrack(() => this.opts.initialFocus.current);
|
||||
if (initialFocus) {
|
||||
// focus the first `data-focused` day node
|
||||
const firstFocusedDay = this.opts.ref.current?.querySelector(`[data-focused]`);
|
||||
if (firstFocusedDay) {
|
||||
firstFocusedDay.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
#setupAccessibleHeadingEffect() {
|
||||
$effect(() => {
|
||||
if (!this.opts.ref.current)
|
||||
return;
|
||||
const removeHeading = createAccessibleHeading({
|
||||
calendarNode: this.opts.ref.current,
|
||||
label: this.fullCalendarLabel,
|
||||
accessibleHeadingId: this.accessibleHeadingId,
|
||||
});
|
||||
return removeHeading;
|
||||
});
|
||||
}
|
||||
#setupFormatterEffect() {
|
||||
$effect.pre(() => {
|
||||
if (this.formatter.getLocale() === this.opts.locale.current)
|
||||
return;
|
||||
this.formatter.setLocale(this.opts.locale.current);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 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 });
|
||||
}
|
||||
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,
|
||||
});
|
||||
});
|
||||
isInvalid = $derived.by(() => {
|
||||
const value = this.opts.value.current;
|
||||
const isDateDisabled = this.opts.isDateDisabled.current;
|
||||
const isDateUnavailable = this.opts.isDateUnavailable.current;
|
||||
if (Array.isArray(value)) {
|
||||
if (!value.length)
|
||||
return false;
|
||||
for (const date of value) {
|
||||
if (isDateDisabled(date))
|
||||
return true;
|
||||
if (isDateUnavailable(date))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!value)
|
||||
return false;
|
||||
if (isDateDisabled(value))
|
||||
return true;
|
||||
if (isDateUnavailable(value))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
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(() => {
|
||||
return `${this.opts.calendarLabel.current} ${this.headingValue}`;
|
||||
});
|
||||
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 && isBefore(maxValue, date))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
isDateSelected(date) {
|
||||
const value = this.opts.value.current;
|
||||
if (Array.isArray(value)) {
|
||||
return value.some((d) => isSameDay(d, date));
|
||||
}
|
||||
else if (!value) {
|
||||
return false;
|
||||
}
|
||||
return isSameDay(value, date);
|
||||
}
|
||||
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,
|
||||
});
|
||||
}
|
||||
#isMultipleSelectionValid(selectedDates) {
|
||||
// only validate for multiple type and when maxDays is set
|
||||
if (this.opts.type.current !== "multiple")
|
||||
return true;
|
||||
if (!this.opts.maxDays.current)
|
||||
return true;
|
||||
const selectedCount = selectedDates.length;
|
||||
if (this.opts.maxDays.current && selectedCount > this.opts.maxDays.current)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
handleCellClick(_, date) {
|
||||
if (this.opts.readonly.current ||
|
||||
this.opts.isDateDisabled.current?.(date) ||
|
||||
this.opts.isDateUnavailable.current?.(date)) {
|
||||
return;
|
||||
}
|
||||
const prev = this.opts.value.current;
|
||||
const multiple = this.opts.type.current === "multiple";
|
||||
if (multiple) {
|
||||
if (Array.isArray(prev) || prev === undefined) {
|
||||
this.opts.value.current = this.handleMultipleUpdate(prev, date);
|
||||
}
|
||||
}
|
||||
else if (!Array.isArray(prev)) {
|
||||
const next = this.handleSingleUpdate(prev, date);
|
||||
if (!next) {
|
||||
this.announcer.announce("Selected date is now empty.", "polite", 5000);
|
||||
}
|
||||
else {
|
||||
this.announcer.announce(`Selected Date: ${this.formatter.selectedDate(next, false)}`, "polite");
|
||||
}
|
||||
this.opts.value.current = getDateWithPreviousTime(next, prev);
|
||||
if (next !== undefined) {
|
||||
this.opts.onDateSelect?.current?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
handleMultipleUpdate(prev, date) {
|
||||
if (!prev) {
|
||||
const newSelection = [date];
|
||||
return this.#isMultipleSelectionValid(newSelection) ? newSelection : [date];
|
||||
}
|
||||
if (!Array.isArray(prev)) {
|
||||
if (DEV)
|
||||
throw new Error("Invalid value for multiple prop.");
|
||||
return;
|
||||
}
|
||||
const index = prev.findIndex((d) => isSameDay(d, date));
|
||||
const preventDeselect = this.opts.preventDeselect.current;
|
||||
if (index === -1) {
|
||||
// adding a new date - check if it would be valid
|
||||
const newSelection = [...prev, date];
|
||||
if (this.#isMultipleSelectionValid(newSelection)) {
|
||||
return newSelection;
|
||||
}
|
||||
else {
|
||||
// reset to just the newly selected date when constraints are violated
|
||||
return [date];
|
||||
}
|
||||
}
|
||||
else if (preventDeselect) {
|
||||
return prev;
|
||||
}
|
||||
else {
|
||||
const next = prev.filter((d) => !isSameDay(d, date));
|
||||
if (!next.length) {
|
||||
this.opts.placeholder.current = date;
|
||||
return undefined;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
}
|
||||
handleSingleUpdate(prev, date) {
|
||||
if (Array.isArray(prev)) {
|
||||
if (DEV)
|
||||
throw new Error("Invalid value for single prop.");
|
||||
}
|
||||
if (!prev)
|
||||
return date;
|
||||
const preventDeselect = this.opts.preventDeselect.current;
|
||||
if (!preventDeselect && isSameDay(prev, date)) {
|
||||
this.opts.placeholder.current = date;
|
||||
return undefined;
|
||||
}
|
||||
return date;
|
||||
}
|
||||
onkeydown(event) {
|
||||
handleCalendarKeydown({
|
||||
event,
|
||||
handleCellClick: this.handleCellClick,
|
||||
shiftFocus: this.shiftFocus,
|
||||
placeholderValue: this.opts.placeholder.current,
|
||||
});
|
||||
}
|
||||
snippetProps = $derived.by(() => ({
|
||||
months: this.months,
|
||||
weekdays: this.weekdays,
|
||||
}));
|
||||
getBitsAttr = (part) => {
|
||||
return calendarAttrs.getAttr(part);
|
||||
};
|
||||
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,
|
||||
}));
|
||||
}
|
||||
export class CalendarHeadingState {
|
||||
static create(opts) {
|
||||
return new CalendarHeadingState(opts, CalendarRootContext.get());
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
attachment;
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
"aria-hidden": boolToStrTrueOrUndef(true),
|
||||
"data-disabled": boolToEmptyStrOrUndef(this.root.opts.disabled.current),
|
||||
"data-readonly": boolToEmptyStrOrUndef(this.root.opts.readonly.current),
|
||||
[this.root.getBitsAttr("heading")]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
const CalendarCellContext = new Context("Calendar.Cell | RangeCalendar.Cell");
|
||||
export class CalendarCellState {
|
||||
static create(opts) {
|
||||
return CalendarCellContext.set(new CalendarCellState(opts, CalendarRootContext.get()));
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
cellDate = $derived.by(() => toDate(this.opts.date.current));
|
||||
isUnavailable = $derived.by(() => this.root.opts.isDateUnavailable.current(this.opts.date.current));
|
||||
isDateToday = $derived.by(() => isToday(this.opts.date.current, getLocalTimeZone()));
|
||||
isOutsideMonth = $derived.by(() => !isSameMonth(this.opts.date.current, this.opts.month.current));
|
||||
isOutsideVisibleMonths = $derived.by(() => this.root.isOutsideVisibleMonths(this.opts.date.current));
|
||||
isDisabled = $derived.by(() => this.root.isDateDisabled(this.opts.date.current) ||
|
||||
(this.isOutsideMonth && this.root.opts.disableDaysOutsideMonth.current));
|
||||
isFocusedDate = $derived.by(() => isSameDay(this.opts.date.current, this.root.opts.placeholder.current));
|
||||
isSelectedDate = $derived.by(() => this.root.isDateSelected(this.opts.date.current));
|
||||
labelText = $derived.by(() => this.root.formatter.custom(this.cellDate, {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
}));
|
||||
attachment;
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
}
|
||||
snippetProps = $derived.by(() => ({
|
||||
disabled: this.isDisabled,
|
||||
unavailable: this.isUnavailable,
|
||||
selected: this.isSelectedDate,
|
||||
day: `${this.opts.date.current.day}`,
|
||||
}));
|
||||
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-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 CalendarDayState {
|
||||
static create(opts) {
|
||||
return new CalendarDayState(opts, CalendarCellContext.get());
|
||||
}
|
||||
opts;
|
||||
cell;
|
||||
attachment;
|
||||
constructor(opts, cell) {
|
||||
this.opts = opts;
|
||||
this.cell = cell;
|
||||
this.onclick = this.onclick.bind(this);
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
}
|
||||
#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);
|
||||
}
|
||||
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,
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class CalendarNextButtonState {
|
||||
static create(opts) {
|
||||
return new CalendarNextButtonState(opts, CalendarRootContext.get());
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
isDisabled = $derived.by(() => this.root.isNextButtonDisabled);
|
||||
attachment;
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.onclick = this.onclick.bind(this);
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
}
|
||||
onclick(_) {
|
||||
if (this.isDisabled)
|
||||
return;
|
||||
this.root.nextPage();
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
role: "button",
|
||||
type: "button",
|
||||
"aria-label": "Next",
|
||||
"aria-disabled": boolToStr(this.isDisabled),
|
||||
"data-disabled": boolToEmptyStrOrUndef(this.isDisabled),
|
||||
disabled: this.isDisabled,
|
||||
[this.root.getBitsAttr("next-button")]: "",
|
||||
//
|
||||
onclick: this.onclick,
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class CalendarPrevButtonState {
|
||||
static create(opts) {
|
||||
return new CalendarPrevButtonState(opts, CalendarRootContext.get());
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
isDisabled = $derived.by(() => this.root.isPrevButtonDisabled);
|
||||
attachment;
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.onclick = this.onclick.bind(this);
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
}
|
||||
onclick(_) {
|
||||
if (this.isDisabled)
|
||||
return;
|
||||
this.root.prevPage();
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
role: "button",
|
||||
type: "button",
|
||||
"aria-label": "Previous",
|
||||
"aria-disabled": boolToStr(this.isDisabled),
|
||||
"data-disabled": boolToEmptyStrOrUndef(this.isDisabled),
|
||||
disabled: this.isDisabled,
|
||||
[this.root.getBitsAttr("prev-button")]: "",
|
||||
//
|
||||
onclick: this.onclick,
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class CalendarGridState {
|
||||
static create(opts) {
|
||||
return new CalendarGridState(opts, CalendarRootContext.get());
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
attachment;
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
tabindex: -1,
|
||||
role: "grid",
|
||||
"aria-readonly": boolToStr(this.root.opts.readonly.current),
|
||||
"aria-disabled": boolToStr(this.root.opts.disabled.current),
|
||||
"data-readonly": boolToEmptyStrOrUndef(this.root.opts.readonly.current),
|
||||
"data-disabled": boolToEmptyStrOrUndef(this.root.opts.disabled.current),
|
||||
[this.root.getBitsAttr("grid")]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class CalendarGridBodyState {
|
||||
static create(opts) {
|
||||
return new CalendarGridBodyState(opts, CalendarRootContext.get());
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
attachment;
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
"data-disabled": boolToEmptyStrOrUndef(this.root.opts.disabled.current),
|
||||
"data-readonly": boolToEmptyStrOrUndef(this.root.opts.readonly.current),
|
||||
[this.root.getBitsAttr("grid-body")]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class CalendarGridHeadState {
|
||||
static create(opts) {
|
||||
return new CalendarGridHeadState(opts, CalendarRootContext.get());
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
attachment;
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
"data-disabled": boolToEmptyStrOrUndef(this.root.opts.disabled.current),
|
||||
"data-readonly": boolToEmptyStrOrUndef(this.root.opts.readonly.current),
|
||||
[this.root.getBitsAttr("grid-head")]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class CalendarGridRowState {
|
||||
static create(opts) {
|
||||
return new CalendarGridRowState(opts, CalendarRootContext.get());
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
attachment;
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
"data-disabled": boolToEmptyStrOrUndef(this.root.opts.disabled.current),
|
||||
"data-readonly": boolToEmptyStrOrUndef(this.root.opts.readonly.current),
|
||||
[this.root.getBitsAttr("grid-row")]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class CalendarHeadCellState {
|
||||
static create(opts) {
|
||||
return new CalendarHeadCellState(opts, CalendarRootContext.get());
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
attachment;
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
"data-disabled": boolToEmptyStrOrUndef(this.root.opts.disabled.current),
|
||||
"data-readonly": boolToEmptyStrOrUndef(this.root.opts.readonly.current),
|
||||
[this.root.getBitsAttr("head-cell")]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class CalendarHeaderState {
|
||||
static create(opts) {
|
||||
return new CalendarHeaderState(opts, CalendarRootContext.get());
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
attachment;
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
"data-disabled": boolToEmptyStrOrUndef(this.root.opts.disabled.current),
|
||||
"data-readonly": boolToEmptyStrOrUndef(this.root.opts.readonly.current),
|
||||
[this.root.getBitsAttr("header")]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class CalendarMonthSelectState {
|
||||
static create(opts) {
|
||||
return new CalendarMonthSelectState(opts, CalendarRootContext.get());
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
attachment;
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.onchange = this.onchange.bind(this);
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
}
|
||||
monthItems = $derived.by(() => {
|
||||
this.root.opts.locale.current;
|
||||
const monthNumbers = this.opts.months.current;
|
||||
const monthFormat = this.opts.monthFormat.current;
|
||||
const months = [];
|
||||
for (const month of monthNumbers) {
|
||||
// create a date with the current year and the month to get localized name
|
||||
const date = this.root.opts.placeholder.current.set({ month });
|
||||
let label;
|
||||
if (typeof monthFormat === "function") {
|
||||
label = monthFormat(month);
|
||||
}
|
||||
else {
|
||||
label = this.root.formatter.custom(toDate(date), { month: monthFormat });
|
||||
}
|
||||
months.push({
|
||||
value: month,
|
||||
label,
|
||||
});
|
||||
}
|
||||
return months;
|
||||
});
|
||||
currentMonth = $derived.by(() => this.root.opts.placeholder.current.month);
|
||||
isDisabled = $derived.by(() => this.root.opts.disabled.current || this.opts.disabled.current);
|
||||
snippetProps = $derived.by(() => {
|
||||
return {
|
||||
monthItems: this.monthItems,
|
||||
selectedMonthItem: this.monthItems.find((month) => month.value === this.currentMonth),
|
||||
};
|
||||
});
|
||||
onchange(event) {
|
||||
if (this.isDisabled)
|
||||
return;
|
||||
const target = event.target;
|
||||
const month = parseInt(target.value, 10);
|
||||
if (!isNaN(month)) {
|
||||
this.root.opts.placeholder.current = this.root.opts.placeholder.current.set({ month });
|
||||
}
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
value: this.currentMonth,
|
||||
disabled: this.isDisabled,
|
||||
"data-disabled": boolToEmptyStrOrUndef(this.isDisabled),
|
||||
[this.root.getBitsAttr("month-select")]: "",
|
||||
//
|
||||
onchange: this.onchange,
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class CalendarYearSelectState {
|
||||
static create(opts) {
|
||||
return new CalendarYearSelectState(opts, CalendarRootContext.get());
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
attachment;
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.onchange = this.onchange.bind(this);
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
}
|
||||
years = $derived.by(() => {
|
||||
if (this.opts.years.current && this.opts.years.current.length)
|
||||
return this.opts.years.current;
|
||||
return this.root.defaultYears;
|
||||
});
|
||||
yearItems = $derived.by(() => {
|
||||
this.root.opts.locale.current;
|
||||
const yearFormat = this.opts.yearFormat.current;
|
||||
const localYears = [];
|
||||
for (const year of this.years) {
|
||||
// create a date with the year to get localized formatting
|
||||
const date = this.root.opts.placeholder.current.set({ year });
|
||||
let label;
|
||||
if (typeof yearFormat === "function") {
|
||||
label = yearFormat(year);
|
||||
}
|
||||
else {
|
||||
label = this.root.formatter.custom(toDate(date), { year: yearFormat });
|
||||
}
|
||||
localYears.push({
|
||||
value: year,
|
||||
label,
|
||||
});
|
||||
}
|
||||
return localYears;
|
||||
});
|
||||
currentYear = $derived.by(() => this.root.opts.placeholder.current.year);
|
||||
isDisabled = $derived.by(() => this.root.opts.disabled.current || this.opts.disabled.current);
|
||||
snippetProps = $derived.by(() => {
|
||||
return {
|
||||
yearItems: this.yearItems,
|
||||
selectedYearItem: this.yearItems.find((year) => year.value === this.currentYear),
|
||||
};
|
||||
});
|
||||
onchange(event) {
|
||||
if (this.isDisabled)
|
||||
return;
|
||||
const target = event.target;
|
||||
const year = parseInt(target.value, 10);
|
||||
if (!isNaN(year)) {
|
||||
this.root.opts.placeholder.current = this.root.opts.placeholder.current.set({ year });
|
||||
}
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
value: this.currentYear,
|
||||
disabled: this.isDisabled,
|
||||
"data-disabled": boolToEmptyStrOrUndef(this.isDisabled),
|
||||
[this.root.getBitsAttr("year-select")]: "",
|
||||
//
|
||||
onchange: this.onchange,
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user