1274 lines
46 KiB
JavaScript
1274 lines
46 KiB
JavaScript
import { boxWith, onDestroyEffect, attachRef, DOMContext, simpleBox, } from "svelte-toolbelt";
|
|
import { onMount, untrack } from "svelte";
|
|
import { Context, watch } from "runed";
|
|
import { createBitsAttrs, boolToStr, boolToStrTrueOrUndef, boolToEmptyStrOrUndef, } from "../../internal/attrs.js";
|
|
import { isBrowser, isNumberString } from "../../internal/is.js";
|
|
import { kbd } from "../../internal/kbd.js";
|
|
import { useId } from "../../internal/use-id.js";
|
|
import { createFormatter } from "../../internal/date-time/formatter.js";
|
|
import { getAnnouncer } from "../../internal/date-time/announcer.js";
|
|
import { areAllSegmentsFilled, createContent, getDefaultHourCycle, getValueFromSegments, inferGranularity, initSegmentStates, initializeSegmentValues, isAcceptableSegmentKey, isDateAndTimeSegmentObj, isDateSegmentPart, isFirstSegment, removeDescriptionElement, setDescription, } from "../../internal/date-time/field/helpers.js";
|
|
import { DATE_SEGMENT_PARTS, EDITABLE_TIME_SEGMENT_PARTS, } from "../../internal/date-time/field/parts.js";
|
|
import { getDaysInMonth, isBefore, toDate } from "../../internal/date-time/utils.js";
|
|
import { getFirstSegment, handleSegmentNavigation, isSegmentNavigationKey, moveToNextSegment, moveToPrevSegment, } from "../../internal/date-time/field/segments.js";
|
|
export const dateFieldAttrs = createBitsAttrs({
|
|
component: "date-field",
|
|
parts: ["input", "label", "segment"],
|
|
});
|
|
const SEGMENT_CONFIGS = {
|
|
day: {
|
|
min: 1,
|
|
max: (root) => {
|
|
const segmentMonthValue = root.segmentValues.month;
|
|
const placeholder = root.value.current ?? root.placeholder.current;
|
|
return segmentMonthValue
|
|
? getDaysInMonth(placeholder.set({ month: Number.parseInt(segmentMonthValue) }))
|
|
: getDaysInMonth(placeholder);
|
|
},
|
|
cycle: 1,
|
|
padZero: true,
|
|
},
|
|
month: {
|
|
min: 1,
|
|
max: 12,
|
|
cycle: 1,
|
|
padZero: true,
|
|
getAnnouncement: (month, root) => {
|
|
if (!root.placeholder.current)
|
|
return "";
|
|
return `${month} - ${root.formatter.fullMonth(toDate(root.placeholder.current.set({ month })))}`;
|
|
},
|
|
},
|
|
year: {
|
|
min: 1,
|
|
max: 9999,
|
|
cycle: 1,
|
|
padZero: false,
|
|
},
|
|
hour: {
|
|
min: (root) => (root.hourCycle.current === 12 ? 1 : 0),
|
|
max: (root) => {
|
|
if (root.hourCycle.current === 24)
|
|
return 23;
|
|
if (root.hourCycle.current === 12)
|
|
return 12;
|
|
// if hourCycle is undefined, infer from locale
|
|
const inferredHourCycle = getDefaultHourCycle(root.locale.current);
|
|
return inferredHourCycle === 12 ? 12 : 23;
|
|
},
|
|
cycle: 1,
|
|
canBeZero: true,
|
|
padZero: true,
|
|
},
|
|
minute: {
|
|
min: 0,
|
|
max: 59,
|
|
cycle: 1,
|
|
canBeZero: true,
|
|
padZero: true,
|
|
},
|
|
second: {
|
|
min: 0,
|
|
max: 59,
|
|
cycle: 1,
|
|
canBeZero: true,
|
|
padZero: true,
|
|
},
|
|
};
|
|
const DateFieldRootContext = new Context("DateField.Root");
|
|
export class DateFieldRootState {
|
|
static create(opts, rangeRoot) {
|
|
return DateFieldRootContext.set(new DateFieldRootState(opts, rangeRoot));
|
|
}
|
|
value;
|
|
placeholder;
|
|
validate;
|
|
minValue;
|
|
maxValue;
|
|
disabled;
|
|
readonly;
|
|
granularity;
|
|
readonlySegments;
|
|
hourCycle;
|
|
locale;
|
|
hideTimeZone;
|
|
required;
|
|
onInvalid;
|
|
errorMessageId;
|
|
isInvalidProp;
|
|
descriptionId = useId();
|
|
formatter;
|
|
initialSegments;
|
|
segmentValues = $state();
|
|
announcer;
|
|
readonlySegmentsSet = $derived.by(() => new Set(this.readonlySegments.current));
|
|
segmentStates = initSegmentStates();
|
|
#fieldNode = $state(null);
|
|
#labelNode = $state(null);
|
|
descriptionNode = $state(null);
|
|
validationNode = $state(null);
|
|
states = initSegmentStates();
|
|
dayPeriodNode = $state(null);
|
|
rangeRoot = undefined;
|
|
name = $state("");
|
|
domContext = new DOMContext(() => null);
|
|
constructor(props, rangeRoot) {
|
|
this.rangeRoot = rangeRoot;
|
|
/**
|
|
* Since the `DateFieldRootState` can be used in two contexts, as a standalone
|
|
* field or as a field within a `DateRangeField` component, we handle assigning
|
|
* the props based on that context.
|
|
*/
|
|
this.value = props.value;
|
|
this.placeholder = rangeRoot ? rangeRoot.opts.placeholder : props.placeholder;
|
|
this.validate = rangeRoot ? simpleBox(undefined) : props.validate;
|
|
this.minValue = rangeRoot ? rangeRoot.opts.minValue : props.minValue;
|
|
this.maxValue = rangeRoot ? rangeRoot.opts.maxValue : props.maxValue;
|
|
this.disabled = rangeRoot ? rangeRoot.opts.disabled : props.disabled;
|
|
this.readonly = rangeRoot ? rangeRoot.opts.readonly : props.readonly;
|
|
this.granularity = rangeRoot ? rangeRoot.opts.granularity : props.granularity;
|
|
this.readonlySegments = rangeRoot
|
|
? rangeRoot.opts.readonlySegments
|
|
: props.readonlySegments;
|
|
this.hourCycle = rangeRoot ? rangeRoot.opts.hourCycle : props.hourCycle;
|
|
this.locale = rangeRoot ? rangeRoot.opts.locale : props.locale;
|
|
this.hideTimeZone = rangeRoot ? rangeRoot.opts.hideTimeZone : props.hideTimeZone;
|
|
this.required = rangeRoot ? rangeRoot.opts.required : props.required;
|
|
this.onInvalid = rangeRoot ? rangeRoot.opts.onInvalid : props.onInvalid;
|
|
this.errorMessageId = rangeRoot ? rangeRoot.opts.errorMessageId : props.errorMessageId;
|
|
this.isInvalidProp = props.isInvalidProp;
|
|
this.formatter = createFormatter({
|
|
initialLocale: this.locale.current,
|
|
monthFormat: boxWith(() => "long"),
|
|
yearFormat: boxWith(() => "numeric"),
|
|
});
|
|
this.initialSegments = initializeSegmentValues(this.inferredGranularity);
|
|
this.segmentValues = this.initialSegments;
|
|
this.announcer = getAnnouncer(null);
|
|
this.getFieldNode = this.getFieldNode.bind(this);
|
|
this.updateSegment = this.updateSegment.bind(this);
|
|
this.handleSegmentClick = this.handleSegmentClick.bind(this);
|
|
this.getBaseSegmentAttrs = this.getBaseSegmentAttrs.bind(this);
|
|
$effect(() => {
|
|
untrack(() => {
|
|
this.initialSegments = initializeSegmentValues(this.inferredGranularity);
|
|
});
|
|
});
|
|
onMount(() => {
|
|
this.announcer = getAnnouncer(this.domContext.getDocument());
|
|
});
|
|
onDestroyEffect(() => {
|
|
if (rangeRoot)
|
|
return;
|
|
removeDescriptionElement(this.descriptionId, this.domContext.getDocument());
|
|
});
|
|
$effect(() => {
|
|
if (rangeRoot)
|
|
return;
|
|
if (this.formatter.getLocale() === this.locale.current)
|
|
return;
|
|
this.formatter.setLocale(this.locale.current);
|
|
});
|
|
$effect(() => {
|
|
if (rangeRoot)
|
|
return;
|
|
if (this.value.current) {
|
|
const descriptionId = untrack(() => this.descriptionId);
|
|
setDescription({
|
|
id: descriptionId,
|
|
formatter: this.formatter,
|
|
value: this.value.current,
|
|
doc: this.domContext.getDocument(),
|
|
});
|
|
}
|
|
const placeholder = untrack(() => this.placeholder.current);
|
|
if (this.value.current && placeholder !== this.value.current) {
|
|
untrack(() => {
|
|
if (this.value.current) {
|
|
this.placeholder.current = this.value.current;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
if (this.value.current) {
|
|
this.syncSegmentValues(this.value.current);
|
|
}
|
|
$effect(() => {
|
|
this.locale.current;
|
|
if (this.value.current) {
|
|
this.syncSegmentValues(this.value.current);
|
|
}
|
|
this.#clearUpdating();
|
|
});
|
|
$effect(() => {
|
|
if (this.value.current === undefined) {
|
|
this.segmentValues = initializeSegmentValues(this.inferredGranularity);
|
|
}
|
|
});
|
|
watch(() => this.validationStatus, () => {
|
|
if (this.validationStatus !== false) {
|
|
this.onInvalid.current?.(this.validationStatus.reason, this.validationStatus.message);
|
|
}
|
|
});
|
|
}
|
|
setName(name) {
|
|
this.name = name;
|
|
}
|
|
/**
|
|
* Sets the field node for the `DateFieldRootState` instance. We use this method so we can
|
|
* keep `#fieldNode` private to prevent accidental usage of the incorrect field node.
|
|
*/
|
|
setFieldNode(node) {
|
|
this.#fieldNode = node;
|
|
}
|
|
/**
|
|
* Gets the correct field node for the date field regardless of whether it's being
|
|
* used in a standalone context or within a `DateRangeField` component.
|
|
*/
|
|
getFieldNode() {
|
|
/** If we're not within a DateRangeField, we return this field. */
|
|
if (!this.rangeRoot) {
|
|
return this.#fieldNode;
|
|
}
|
|
else {
|
|
/**
|
|
* Otherwise, we return the rangeRoot's field node which
|
|
* contains both start and end fields.
|
|
*/
|
|
return this.rangeRoot.fieldNode;
|
|
}
|
|
}
|
|
/**
|
|
* Sets the label node for the `DateFieldRootState` instance. We use this method so we can
|
|
* keep `#labelNode` private to prevent accidental usage of the incorrect label node.
|
|
*/
|
|
setLabelNode(node) {
|
|
this.#labelNode = node;
|
|
}
|
|
/**
|
|
* Gets the correct label node for the date field regardless of whether it's being used in
|
|
* a standalone context or within a `DateRangeField` component.
|
|
*/
|
|
getLabelNode() {
|
|
/** If we're not within a DateRangeField, we return this field. */
|
|
if (!this.rangeRoot) {
|
|
return this.#labelNode;
|
|
}
|
|
/** Otherwise we return the rangeRoot's label node. */
|
|
return this.rangeRoot.labelNode;
|
|
}
|
|
#clearUpdating() {
|
|
this.states.day.updating = null;
|
|
this.states.month.updating = null;
|
|
this.states.year.updating = null;
|
|
this.states.hour.updating = null;
|
|
this.states.minute.updating = null;
|
|
this.states.dayPeriod.updating = null;
|
|
}
|
|
setValue(value) {
|
|
this.value.current = value;
|
|
}
|
|
syncSegmentValues(value) {
|
|
const dateValues = DATE_SEGMENT_PARTS.map((part) => {
|
|
const partValue = value[part];
|
|
if (part === "month") {
|
|
if (this.states.month.updating) {
|
|
return [part, this.states.month.updating];
|
|
}
|
|
if (partValue < 10) {
|
|
return [part, `0${partValue}`];
|
|
}
|
|
}
|
|
if (part === "day") {
|
|
if (this.states.day.updating) {
|
|
return [part, this.states.day.updating];
|
|
}
|
|
if (partValue < 10) {
|
|
return [part, `0${partValue}`];
|
|
}
|
|
}
|
|
if (part === "year") {
|
|
if (this.states.year.updating) {
|
|
return [part, this.states.year.updating];
|
|
}
|
|
const valueDigits = `${partValue}`.length;
|
|
const diff = 4 - valueDigits;
|
|
if (diff > 0) {
|
|
return [part, `${"0".repeat(diff)}${partValue}`];
|
|
}
|
|
}
|
|
return [part, `${partValue}`];
|
|
});
|
|
if ("hour" in value) {
|
|
const timeValues = EDITABLE_TIME_SEGMENT_PARTS.map((part) => {
|
|
if (part === "dayPeriod") {
|
|
if (this.states.dayPeriod.updating) {
|
|
return [part, this.states.dayPeriod.updating];
|
|
}
|
|
else {
|
|
return [part, this.formatter.dayPeriod(toDate(value))];
|
|
}
|
|
}
|
|
else if (part === "hour") {
|
|
if (this.states.hour.updating) {
|
|
return [part, this.states.hour.updating];
|
|
}
|
|
if (value[part] !== undefined && value[part] < 10) {
|
|
return [part, `0${value[part]}`];
|
|
}
|
|
if (value[part] === 0) {
|
|
/**
|
|
* If we're rendering a `dayPeriod` segment, we're operating in a
|
|
* 12-hour clock, so we never allow the displayed hour to be 0.
|
|
*/
|
|
if (this.dayPeriodNode) {
|
|
return [part, "12"];
|
|
}
|
|
}
|
|
}
|
|
else if (part === "minute") {
|
|
if (this.states.minute.updating) {
|
|
return [part, this.states.minute.updating];
|
|
}
|
|
if (value[part] !== undefined && value[part] < 10) {
|
|
return [part, `0${value[part]}`];
|
|
}
|
|
}
|
|
else if (part === "second") {
|
|
if (this.states.second.updating) {
|
|
return [part, this.states.second.updating];
|
|
}
|
|
if (value[part] !== undefined && value[part] < 10) {
|
|
return [part, `0${value[part]}`];
|
|
}
|
|
}
|
|
return [part, `${value[part]}`];
|
|
});
|
|
const mergedSegmentValues = [...dateValues, ...timeValues];
|
|
this.segmentValues = Object.fromEntries(mergedSegmentValues);
|
|
this.#clearUpdating();
|
|
return;
|
|
}
|
|
this.segmentValues = Object.fromEntries(dateValues);
|
|
}
|
|
validationStatus = $derived.by(() => {
|
|
const value = this.value.current;
|
|
if (!value)
|
|
return false;
|
|
const msg = this.validate.current?.(value);
|
|
if (msg) {
|
|
return {
|
|
reason: "custom",
|
|
message: msg,
|
|
};
|
|
}
|
|
const minValue = this.minValue.current;
|
|
if (minValue && isBefore(value, minValue)) {
|
|
return {
|
|
reason: "min",
|
|
};
|
|
}
|
|
const maxValue = this.maxValue.current;
|
|
if (maxValue && isBefore(maxValue, value)) {
|
|
return {
|
|
reason: "max",
|
|
};
|
|
}
|
|
return false;
|
|
});
|
|
isInvalid = $derived.by(() => {
|
|
if (this.validationStatus === false)
|
|
return false;
|
|
if (this.isInvalidProp.current)
|
|
return true;
|
|
return true;
|
|
});
|
|
inferredGranularity = $derived.by(() => {
|
|
const granularity = this.granularity.current;
|
|
if (granularity)
|
|
return granularity;
|
|
const inferred = inferGranularity(this.placeholder.current, this.granularity.current);
|
|
return inferred;
|
|
});
|
|
dateRef = $derived.by(() => this.value.current !== undefined ? this.value.current : this.placeholder.current);
|
|
allSegmentContent = $derived.by(() => {
|
|
return createContent({
|
|
segmentValues: this.segmentValues,
|
|
formatter: this.formatter,
|
|
locale: this.locale.current,
|
|
granularity: this.inferredGranularity,
|
|
dateRef: this.dateRef,
|
|
hideTimeZone: this.hideTimeZone.current,
|
|
hourCycle: this.hourCycle.current,
|
|
});
|
|
});
|
|
segmentContents = $derived.by(() => this.allSegmentContent.arr);
|
|
sharedSegmentAttrs = {
|
|
role: "spinbutton",
|
|
contenteditable: "true",
|
|
tabindex: 0,
|
|
spellcheck: false,
|
|
inputmode: "numeric",
|
|
autocorrect: "off",
|
|
enterkeyhint: "next",
|
|
style: {
|
|
caretColor: "transparent",
|
|
},
|
|
};
|
|
#getLabelledBy(segmentId) {
|
|
return `${segmentId} ${this.getLabelNode()?.id ?? ""}`;
|
|
}
|
|
updateSegment(part, cb) {
|
|
const disabled = this.disabled.current;
|
|
const readonly = this.readonly.current;
|
|
const readonlySegmentsSet = this.readonlySegmentsSet;
|
|
if (disabled || readonly || readonlySegmentsSet.has(part))
|
|
return;
|
|
const prev = this.segmentValues;
|
|
let newSegmentValues = prev;
|
|
const dateRef = this.placeholder.current;
|
|
if (isDateAndTimeSegmentObj(prev)) {
|
|
const pVal = prev[part];
|
|
const castCb = cb;
|
|
if (part === "month") {
|
|
const next = castCb(pVal);
|
|
this.states.month.updating = next;
|
|
if (next !== null && prev.day !== null) {
|
|
const date = dateRef.set({ month: Number.parseInt(next) });
|
|
const daysInMonth = getDaysInMonth(toDate(date));
|
|
const prevDay = Number.parseInt(prev.day);
|
|
if (prevDay > daysInMonth) {
|
|
prev.day = `${daysInMonth}`;
|
|
}
|
|
}
|
|
newSegmentValues = { ...prev, [part]: next };
|
|
}
|
|
else if (part === "dayPeriod") {
|
|
const next = castCb(pVal);
|
|
this.states.dayPeriod.updating = next;
|
|
const date = this.value.current;
|
|
if (date && "hour" in date) {
|
|
const trueHour = date.hour;
|
|
if (next === "AM") {
|
|
if (trueHour >= 12) {
|
|
prev.hour = `${trueHour - 12}`;
|
|
}
|
|
}
|
|
else if (next === "PM") {
|
|
if (trueHour < 12) {
|
|
prev.hour = `${trueHour + 12}`;
|
|
}
|
|
}
|
|
}
|
|
newSegmentValues = { ...prev, [part]: next };
|
|
}
|
|
else if (part === "hour") {
|
|
const next = castCb(pVal);
|
|
this.states.hour.updating = next;
|
|
if (next !== null && prev.dayPeriod !== null) {
|
|
const dayPeriod = this.formatter.dayPeriod(toDate(dateRef.set({ hour: Number.parseInt(next) })), this.hourCycle.current);
|
|
if (dayPeriod === "AM" || dayPeriod === "PM") {
|
|
prev.dayPeriod = dayPeriod;
|
|
}
|
|
}
|
|
newSegmentValues = { ...prev, [part]: next };
|
|
}
|
|
else if (part === "minute") {
|
|
const next = castCb(pVal);
|
|
this.states.minute.updating = next;
|
|
newSegmentValues = { ...prev, [part]: next };
|
|
}
|
|
else if (part === "second") {
|
|
const next = castCb(pVal);
|
|
this.states.second.updating = next;
|
|
newSegmentValues = { ...prev, [part]: next };
|
|
}
|
|
else if (part === "year") {
|
|
const next = castCb(pVal);
|
|
this.states.year.updating = next;
|
|
newSegmentValues = { ...prev, [part]: next };
|
|
}
|
|
else if (part === "day") {
|
|
const next = castCb(pVal);
|
|
this.states.day.updating = next;
|
|
newSegmentValues = { ...prev, [part]: next };
|
|
}
|
|
else {
|
|
const next = castCb(pVal);
|
|
newSegmentValues = { ...prev, [part]: next };
|
|
}
|
|
}
|
|
else if (isDateSegmentPart(part)) {
|
|
const pVal = prev[part];
|
|
const castCb = cb;
|
|
const next = castCb(pVal);
|
|
if (part === "month" && next !== null && prev.day !== null) {
|
|
this.states.month.updating = next;
|
|
const date = dateRef.set({ month: Number.parseInt(next) });
|
|
const daysInMonth = getDaysInMonth(toDate(date));
|
|
if (Number.parseInt(prev.day) > daysInMonth) {
|
|
prev.day = `${daysInMonth}`;
|
|
}
|
|
newSegmentValues = { ...prev, [part]: next };
|
|
}
|
|
else if (part === "year") {
|
|
const next = castCb(pVal);
|
|
this.states.year.updating = next;
|
|
newSegmentValues = { ...prev, [part]: next };
|
|
}
|
|
else if (part === "day") {
|
|
const next = castCb(pVal);
|
|
this.states.day.updating = next;
|
|
newSegmentValues = { ...prev, [part]: next };
|
|
}
|
|
else {
|
|
newSegmentValues = { ...prev, [part]: next };
|
|
}
|
|
}
|
|
this.segmentValues = newSegmentValues;
|
|
if (areAllSegmentsFilled(newSegmentValues, this.#fieldNode)) {
|
|
this.setValue(getValueFromSegments({
|
|
segmentObj: newSegmentValues,
|
|
fieldNode: this.#fieldNode,
|
|
dateRef: this.placeholder.current,
|
|
}));
|
|
}
|
|
else {
|
|
this.setValue(undefined);
|
|
this.segmentValues = newSegmentValues;
|
|
}
|
|
}
|
|
handleSegmentClick(e) {
|
|
if (this.disabled.current) {
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
getBaseSegmentAttrs(part, segmentId) {
|
|
const inReadonlySegments = this.readonlySegmentsSet.has(part);
|
|
const defaultAttrs = {
|
|
"aria-invalid": boolToStrTrueOrUndef(this.isInvalid),
|
|
"aria-disabled": boolToStr(this.disabled.current),
|
|
"aria-readonly": boolToStr(this.readonly.current || inReadonlySegments),
|
|
"data-invalid": boolToEmptyStrOrUndef(this.isInvalid),
|
|
"data-disabled": boolToEmptyStrOrUndef(this.disabled.current),
|
|
"data-readonly": boolToEmptyStrOrUndef(this.readonly.current || inReadonlySegments),
|
|
"data-segment": `${part}`,
|
|
[dateFieldAttrs.segment]: "",
|
|
};
|
|
if (part === "literal")
|
|
return defaultAttrs;
|
|
const descriptionId = this.descriptionNode?.id;
|
|
const hasDescription = isFirstSegment(segmentId, this.#fieldNode) && descriptionId;
|
|
const errorMsgId = this.errorMessageId?.current;
|
|
const describedBy = hasDescription
|
|
? `${descriptionId} ${this.isInvalid && errorMsgId ? errorMsgId : ""}`
|
|
: undefined;
|
|
const contenteditable = !(this.readonly.current ||
|
|
inReadonlySegments ||
|
|
this.disabled.current);
|
|
return {
|
|
...defaultAttrs,
|
|
"aria-labelledby": this.#getLabelledBy(segmentId),
|
|
contenteditable: contenteditable ? "true" : undefined,
|
|
"aria-describedby": describedBy,
|
|
tabindex: this.disabled.current ? undefined : 0,
|
|
};
|
|
}
|
|
}
|
|
export class DateFieldInputState {
|
|
static create(opts) {
|
|
return new DateFieldInputState(opts, DateFieldRootContext.get());
|
|
}
|
|
opts;
|
|
root;
|
|
domContext;
|
|
attachment;
|
|
constructor(opts, root) {
|
|
this.opts = opts;
|
|
this.root = root;
|
|
this.domContext = new DOMContext(opts.ref);
|
|
this.root.domContext = this.domContext;
|
|
this.attachment = attachRef(opts.ref, (v) => this.root.setFieldNode(v));
|
|
watch(() => this.opts.name.current, (v) => {
|
|
this.root.setName(v);
|
|
});
|
|
}
|
|
#ariaDescribedBy = $derived.by(() => {
|
|
if (!isBrowser)
|
|
return undefined;
|
|
const doesDescriptionExist = this.domContext.getElementById(this.root.descriptionId);
|
|
if (!doesDescriptionExist)
|
|
return undefined;
|
|
return this.root.descriptionId;
|
|
});
|
|
props = $derived.by(() => ({
|
|
id: this.opts.id.current,
|
|
role: "group",
|
|
"aria-labelledby": this.root.getLabelNode()?.id ?? undefined,
|
|
"aria-describedby": this.#ariaDescribedBy,
|
|
"aria-disabled": boolToStr(this.root.disabled.current),
|
|
"data-invalid": this.root.isInvalid ? "" : undefined,
|
|
"data-disabled": boolToEmptyStrOrUndef(this.root.disabled.current),
|
|
[dateFieldAttrs.input]: "",
|
|
...this.attachment,
|
|
}));
|
|
}
|
|
export class DateFieldHiddenInputState {
|
|
static create() {
|
|
return new DateFieldHiddenInputState(DateFieldRootContext.get());
|
|
}
|
|
root;
|
|
shouldRender = $derived.by(() => this.root.name !== "");
|
|
isoValue = $derived.by(() => this.root.value.current ? this.root.value.current.toString() : "");
|
|
constructor(root) {
|
|
this.root = root;
|
|
}
|
|
props = $derived.by(() => {
|
|
return {
|
|
name: this.root.name,
|
|
value: this.isoValue,
|
|
required: this.root.required.current,
|
|
};
|
|
});
|
|
}
|
|
export class DateFieldLabelState {
|
|
static create(opts) {
|
|
return new DateFieldLabelState(opts, DateFieldRootContext.get());
|
|
}
|
|
opts;
|
|
root;
|
|
attachment;
|
|
constructor(opts, root) {
|
|
this.opts = opts;
|
|
this.root = root;
|
|
this.onclick = this.onclick.bind(this);
|
|
this.attachment = attachRef(opts.ref, (v) => this.root.setLabelNode(v));
|
|
}
|
|
onclick(_) {
|
|
if (this.root.disabled.current)
|
|
return;
|
|
const firstSegment = getFirstSegment(this.root.getFieldNode());
|
|
if (!firstSegment)
|
|
return;
|
|
firstSegment.focus();
|
|
}
|
|
props = $derived.by(() => ({
|
|
id: this.opts.id.current,
|
|
"data-invalid": boolToEmptyStrOrUndef(this.root.isInvalid),
|
|
"data-disabled": boolToEmptyStrOrUndef(this.root.disabled.current),
|
|
[dateFieldAttrs.label]: "",
|
|
onclick: this.onclick,
|
|
...this.attachment,
|
|
}));
|
|
}
|
|
// Base class for numeric segments
|
|
class BaseNumericSegmentState {
|
|
opts;
|
|
root;
|
|
announcer;
|
|
part;
|
|
config;
|
|
attachment;
|
|
constructor(opts, root, part, config) {
|
|
this.opts = opts;
|
|
this.root = root;
|
|
this.part = part;
|
|
this.config = config;
|
|
this.announcer = root.announcer;
|
|
this.onkeydown = this.onkeydown.bind(this);
|
|
this.onfocusout = this.onfocusout.bind(this);
|
|
this.attachment = attachRef(opts.ref);
|
|
}
|
|
#getMax() {
|
|
return typeof this.config.max === "function" ? this.config.max(this.root) : this.config.max;
|
|
}
|
|
#getMin() {
|
|
return typeof this.config.min === "function" ? this.config.min(this.root) : this.config.min;
|
|
}
|
|
#getAnnouncement(value) {
|
|
if (this.config.getAnnouncement) {
|
|
return this.config.getAnnouncement(value, this.root);
|
|
}
|
|
return value;
|
|
}
|
|
#formatValue(value, forDisplay = true) {
|
|
const str = String(value);
|
|
if (forDisplay && this.config.padZero && str.length === 1) {
|
|
return `0${value}`;
|
|
}
|
|
return str;
|
|
}
|
|
onkeydown(e) {
|
|
const placeholder = this.root.value.current ?? this.root.placeholder.current;
|
|
if (e.ctrlKey || e.metaKey || this.root.disabled.current)
|
|
return;
|
|
// Special check for time segments
|
|
if ((this.part === "hour" || this.part === "minute" || this.part === "second") &&
|
|
!(this.part in placeholder))
|
|
return;
|
|
if (e.key !== kbd.TAB)
|
|
e.preventDefault();
|
|
if (!isAcceptableSegmentKey(e.key))
|
|
return;
|
|
if (isArrowUp(e.key)) {
|
|
this.#handleArrowUp(placeholder);
|
|
return;
|
|
}
|
|
if (isArrowDown(e.key)) {
|
|
this.#handleArrowDown(placeholder);
|
|
return;
|
|
}
|
|
if (isNumberString(e.key)) {
|
|
this.#handleNumberKey(e);
|
|
return;
|
|
}
|
|
if (isBackspace(e.key)) {
|
|
this.#handleBackspace(e);
|
|
return;
|
|
}
|
|
if (isSegmentNavigationKey(e.key)) {
|
|
handleSegmentNavigation(e, this.root.getFieldNode());
|
|
}
|
|
}
|
|
#handleArrowUp(placeholder) {
|
|
const stateKey = this.part;
|
|
if (stateKey in this.root.states) {
|
|
this.root.states[stateKey].hasLeftFocus = false;
|
|
}
|
|
// @ts-expect-error this is a part
|
|
this.root.updateSegment(this.part, (prev) => {
|
|
if (prev === null) {
|
|
const next = placeholder[this.part];
|
|
this.announcer.announce(this.#getAnnouncement(next));
|
|
return this.#formatValue(next);
|
|
}
|
|
const current = placeholder.set({
|
|
[this.part]: Number.parseInt(prev),
|
|
});
|
|
// @ts-expect-error this is a part
|
|
const next = current.cycle(this.part, this.config.cycle)[this.part];
|
|
this.announcer.announce(this.#getAnnouncement(next));
|
|
return this.#formatValue(next);
|
|
});
|
|
}
|
|
#handleArrowDown(placeholder) {
|
|
const stateKey = this.part;
|
|
if (stateKey in this.root.states) {
|
|
this.root.states[stateKey].hasLeftFocus = false;
|
|
}
|
|
// @ts-expect-error this is a part
|
|
this.root.updateSegment(this.part, (prev) => {
|
|
if (prev === null) {
|
|
const next = placeholder[this.part];
|
|
this.announcer.announce(this.#getAnnouncement(next));
|
|
return this.#formatValue(next);
|
|
}
|
|
const current = placeholder.set({
|
|
[this.part]: Number.parseInt(prev),
|
|
});
|
|
// @ts-expect-error this is a part
|
|
const next = current.cycle(this.part, -this.config.cycle)[this.part];
|
|
this.announcer.announce(this.#getAnnouncement(next));
|
|
return this.#formatValue(next);
|
|
});
|
|
}
|
|
#handleNumberKey(e) {
|
|
const num = Number.parseInt(e.key);
|
|
let moveToNext = false;
|
|
const max = this.#getMax();
|
|
const maxStart = Math.floor(max / 10);
|
|
const numIsZero = num === 0;
|
|
const stateKey = this.part;
|
|
// @ts-expect-error this is a part
|
|
this.root.updateSegment(this.part, (prev) => {
|
|
// Check if user has left focus
|
|
if (stateKey in this.root.states && this.root.states[stateKey].hasLeftFocus) {
|
|
prev = null;
|
|
this.root.states[stateKey].hasLeftFocus = false;
|
|
}
|
|
// Starting fresh
|
|
if (prev === null) {
|
|
if (numIsZero) {
|
|
if (stateKey in this.root.states) {
|
|
this.root.states[stateKey].lastKeyZero = true;
|
|
}
|
|
this.announcer.announce("0");
|
|
return "0";
|
|
}
|
|
if (stateKey in this.root.states &&
|
|
(this.root.states[stateKey].lastKeyZero || num > maxStart)) {
|
|
moveToNext = true;
|
|
}
|
|
if (stateKey in this.root.states) {
|
|
this.root.states[stateKey].lastKeyZero = false;
|
|
}
|
|
if (moveToNext && String(num).length === 1) {
|
|
this.announcer.announce(num);
|
|
return `0${num}`;
|
|
}
|
|
return `${num}`;
|
|
}
|
|
// Handle special cases for segments with lastKeyZero tracking
|
|
if (stateKey in this.root.states && this.root.states[stateKey].lastKeyZero) {
|
|
if (num !== 0) {
|
|
moveToNext = true;
|
|
this.root.states[stateKey].lastKeyZero = false;
|
|
return `0${num}`;
|
|
}
|
|
// Special handling for hour segment with 24-hour cycle
|
|
if (this.part === "hour" && num === 0 && this.root.hourCycle.current === 24) {
|
|
moveToNext = true;
|
|
this.root.states[stateKey].lastKeyZero = false;
|
|
return `00`;
|
|
}
|
|
// Special handling for minute/second segments
|
|
if ((this.part === "minute" || this.part === "second") && num === 0) {
|
|
moveToNext = true;
|
|
this.root.states[stateKey].lastKeyZero = false;
|
|
return "00";
|
|
}
|
|
return prev;
|
|
}
|
|
const total = Number.parseInt(prev + num.toString());
|
|
if (total > max) {
|
|
moveToNext = true;
|
|
return `0${num}`;
|
|
}
|
|
moveToNext = true;
|
|
return `${total}`;
|
|
});
|
|
if (moveToNext) {
|
|
moveToNextSegment(e, this.root.getFieldNode());
|
|
}
|
|
}
|
|
#handleBackspace(e) {
|
|
const stateKey = this.part;
|
|
if (stateKey in this.root.states) {
|
|
this.root.states[stateKey].hasLeftFocus = false;
|
|
}
|
|
let moveToPrev = false;
|
|
// @ts-expect-error this is a part
|
|
this.root.updateSegment(this.part, (prev) => {
|
|
if (prev === null) {
|
|
moveToPrev = true;
|
|
this.announcer.announce(null);
|
|
return null;
|
|
}
|
|
if (prev.length === 2 && prev.startsWith("0")) {
|
|
this.announcer.announce(null);
|
|
return null;
|
|
}
|
|
const str = prev.toString();
|
|
if (str.length === 1) {
|
|
this.announcer.announce(null);
|
|
return null;
|
|
}
|
|
const next = Number.parseInt(str.slice(0, -1));
|
|
this.announcer.announce(this.#getAnnouncement(next));
|
|
return `${next}`;
|
|
});
|
|
if (moveToPrev) {
|
|
moveToPrevSegment(e, this.root.getFieldNode());
|
|
}
|
|
}
|
|
onfocusout(_) {
|
|
const stateKey = this.part;
|
|
if (stateKey in this.root.states) {
|
|
this.root.states[stateKey].hasLeftFocus = true;
|
|
}
|
|
// Pad with zero if needed
|
|
if (this.config.padZero) {
|
|
// @ts-expect-error this is a part
|
|
this.root.updateSegment(this.part, (prev) => {
|
|
if (prev && prev.length === 1) {
|
|
return `0${prev}`;
|
|
}
|
|
return prev;
|
|
});
|
|
}
|
|
}
|
|
getSegmentProps() {
|
|
const segmentValues = this.root.segmentValues;
|
|
const placeholder = this.root.placeholder.current;
|
|
const isEmpty = segmentValues[this.part] === null;
|
|
let date = placeholder;
|
|
if (segmentValues[this.part]) {
|
|
date = placeholder.set({
|
|
[this.part]: Number.parseInt(segmentValues[this.part]),
|
|
});
|
|
}
|
|
const valueNow = date[this.part];
|
|
const valueMin = this.#getMin();
|
|
const valueMax = this.#getMax();
|
|
let valueText = isEmpty ? "Empty" : `${valueNow}`;
|
|
// Special handling for hour segment with dayPeriod
|
|
if (this.part === "hour" && "dayPeriod" in segmentValues && segmentValues.dayPeriod) {
|
|
valueText = isEmpty ? "Empty" : `${valueNow} ${segmentValues.dayPeriod}`;
|
|
}
|
|
return {
|
|
"aria-label": `${this.part}, `,
|
|
"aria-valuemin": valueMin,
|
|
"aria-valuemax": valueMax,
|
|
"aria-valuenow": valueNow,
|
|
"aria-valuetext": valueText,
|
|
};
|
|
}
|
|
props = $derived.by(() => {
|
|
return {
|
|
...this.root.sharedSegmentAttrs,
|
|
id: this.opts.id.current,
|
|
...this.getSegmentProps(),
|
|
onkeydown: this.onkeydown,
|
|
onfocusout: this.onfocusout,
|
|
onclick: this.root.handleSegmentClick,
|
|
...this.root.getBaseSegmentAttrs(this.part, this.opts.id.current),
|
|
...this.attachment,
|
|
};
|
|
});
|
|
}
|
|
class DateFieldYearSegmentState extends BaseNumericSegmentState {
|
|
#pressedKeys = [];
|
|
#backspaceCount = 0;
|
|
constructor(opts, root) {
|
|
super(opts, root, "year", SEGMENT_CONFIGS.year);
|
|
}
|
|
onkeydown(e) {
|
|
if (e.ctrlKey || e.metaKey || this.root.disabled.current)
|
|
return;
|
|
if (e.key !== kbd.TAB)
|
|
e.preventDefault();
|
|
if (!isAcceptableSegmentKey(e.key))
|
|
return;
|
|
if (isArrowUp(e.key)) {
|
|
this.#resetBackspaceCount();
|
|
super.onkeydown(e);
|
|
return;
|
|
}
|
|
if (isArrowDown(e.key)) {
|
|
this.#resetBackspaceCount();
|
|
super.onkeydown(e);
|
|
return;
|
|
}
|
|
if (isNumberString(e.key)) {
|
|
this.#handleYearNumberKey(e);
|
|
return;
|
|
}
|
|
if (isBackspace(e.key)) {
|
|
this.#handleYearBackspace(e);
|
|
return;
|
|
}
|
|
if (isSegmentNavigationKey(e.key)) {
|
|
handleSegmentNavigation(e, this.root.getFieldNode());
|
|
}
|
|
}
|
|
#resetBackspaceCount() {
|
|
this.#backspaceCount = 0;
|
|
}
|
|
#incrementBackspaceCount() {
|
|
this.#backspaceCount++;
|
|
}
|
|
#handleYearNumberKey(e) {
|
|
this.#pressedKeys.push(e.key);
|
|
let moveToNext = false;
|
|
const num = Number.parseInt(e.key);
|
|
this.root.updateSegment("year", (prev) => {
|
|
if (this.root.states.year.hasLeftFocus) {
|
|
prev = null;
|
|
this.root.states.year.hasLeftFocus = false;
|
|
}
|
|
if (prev === null) {
|
|
this.announcer.announce(num);
|
|
return `000${num}`;
|
|
}
|
|
const str = prev.toString() + num.toString();
|
|
const mergedInt = Number.parseInt(str);
|
|
const mergedIntDigits = String(mergedInt).length;
|
|
if (mergedIntDigits < 4) {
|
|
if (this.#backspaceCount > 0 &&
|
|
this.#pressedKeys.length <= this.#backspaceCount &&
|
|
str.length <= 4) {
|
|
this.announcer.announce(mergedInt);
|
|
return str;
|
|
}
|
|
this.announcer.announce(mergedInt);
|
|
return prependYearZeros(mergedInt);
|
|
}
|
|
this.announcer.announce(mergedInt);
|
|
moveToNext = true;
|
|
const mergedIntStr = `${mergedInt}`;
|
|
if (mergedIntStr.length > 4) {
|
|
return mergedIntStr.slice(0, 4);
|
|
}
|
|
return mergedIntStr;
|
|
});
|
|
if (this.#pressedKeys.length === 4 || this.#pressedKeys.length === this.#backspaceCount) {
|
|
moveToNext = true;
|
|
}
|
|
if (moveToNext) {
|
|
moveToNextSegment(e, this.root.getFieldNode());
|
|
}
|
|
}
|
|
#handleYearBackspace(e) {
|
|
this.#pressedKeys = [];
|
|
this.#incrementBackspaceCount();
|
|
let moveToPrev = false;
|
|
this.root.updateSegment("year", (prev) => {
|
|
this.root.states.year.hasLeftFocus = false;
|
|
if (prev === null) {
|
|
moveToPrev = true;
|
|
this.announcer.announce(null);
|
|
return null;
|
|
}
|
|
const str = prev.toString();
|
|
if (str.length === 1) {
|
|
this.announcer.announce(null);
|
|
return null;
|
|
}
|
|
const next = str.slice(0, -1);
|
|
this.announcer.announce(next);
|
|
return `${next}`;
|
|
});
|
|
if (moveToPrev) {
|
|
moveToPrevSegment(e, this.root.getFieldNode());
|
|
}
|
|
}
|
|
onfocusout(_) {
|
|
this.root.states.year.hasLeftFocus = true;
|
|
this.#pressedKeys = [];
|
|
this.#resetBackspaceCount();
|
|
this.root.updateSegment("year", (prev) => {
|
|
if (prev && prev.length !== 4) {
|
|
return prependYearZeros(Number.parseInt(prev));
|
|
}
|
|
return prev;
|
|
});
|
|
}
|
|
}
|
|
class DateFieldDaySegmentState extends BaseNumericSegmentState {
|
|
constructor(opts, root) {
|
|
super(opts, root, "day", SEGMENT_CONFIGS.day);
|
|
}
|
|
}
|
|
class DateFieldMonthSegmentState extends BaseNumericSegmentState {
|
|
constructor(opts, root) {
|
|
super(opts, root, "month", SEGMENT_CONFIGS.month);
|
|
}
|
|
}
|
|
class DateFieldHourSegmentState extends BaseNumericSegmentState {
|
|
constructor(opts, root) {
|
|
super(opts, root, "hour", SEGMENT_CONFIGS.hour);
|
|
}
|
|
// Override to handle special hour logic
|
|
onkeydown(e) {
|
|
// Add special handling for hour display with dayPeriod
|
|
if (isNumberString(e.key)) {
|
|
const oldUpdateSegment = this.root.updateSegment.bind(this.root);
|
|
// oxlint-disable-next-line no-explicit-any
|
|
this.root.updateSegment = (part, cb) => {
|
|
const result = oldUpdateSegment(part, cb);
|
|
// After updating hour, check if we need to display "12" instead of "0"
|
|
if (part === "hour" && "hour" in this.root.segmentValues) {
|
|
const hourValue = this.root.segmentValues.hour;
|
|
if (hourValue === "0" &&
|
|
this.root.dayPeriodNode &&
|
|
this.root.hourCycle.current !== 24) {
|
|
this.root.segmentValues.hour = "12";
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
}
|
|
super.onkeydown(e);
|
|
// Restore original updateSegment
|
|
this.root.updateSegment = this.root.updateSegment.bind(this.root);
|
|
}
|
|
}
|
|
class DateFieldMinuteSegmentState extends BaseNumericSegmentState {
|
|
constructor(opts, root) {
|
|
super(opts, root, "minute", SEGMENT_CONFIGS.minute);
|
|
}
|
|
}
|
|
class DateFieldSecondSegmentState extends BaseNumericSegmentState {
|
|
constructor(opts, root) {
|
|
super(opts, root, "second", SEGMENT_CONFIGS.second);
|
|
}
|
|
}
|
|
export class DateFieldDayPeriodSegmentState {
|
|
static create(opts) {
|
|
return new DateFieldDayPeriodSegmentState(opts, DateFieldRootContext.get());
|
|
}
|
|
opts;
|
|
root;
|
|
attachment;
|
|
#announcer;
|
|
constructor(opts, root) {
|
|
this.opts = opts;
|
|
this.root = root;
|
|
this.#announcer = this.root.announcer;
|
|
this.onkeydown = this.onkeydown.bind(this);
|
|
this.attachment = attachRef(opts.ref, (v) => (this.root.dayPeriodNode = v));
|
|
}
|
|
onkeydown(e) {
|
|
if (e.ctrlKey || e.metaKey || this.root.disabled.current)
|
|
return;
|
|
if (e.key !== kbd.TAB)
|
|
e.preventDefault();
|
|
if (!isAcceptableDayPeriodKey(e.key))
|
|
return;
|
|
if (isArrowUp(e.key) || isArrowDown(e.key)) {
|
|
this.root.updateSegment("dayPeriod", (prev) => {
|
|
if (prev === "AM") {
|
|
const next = "PM";
|
|
this.#announcer.announce(next);
|
|
return next;
|
|
}
|
|
const next = "AM";
|
|
this.#announcer.announce(next);
|
|
return next;
|
|
});
|
|
return;
|
|
}
|
|
if (isBackspace(e.key)) {
|
|
this.root.states.dayPeriod.hasLeftFocus = false;
|
|
this.root.updateSegment("dayPeriod", () => {
|
|
const next = "AM";
|
|
this.#announcer.announce(next);
|
|
return next;
|
|
});
|
|
}
|
|
if (e.key === kbd.A || e.key === kbd.P || kbd.a || kbd.p) {
|
|
this.root.updateSegment("dayPeriod", () => {
|
|
const next = e.key === kbd.A || e.key === kbd.a ? "AM" : "PM";
|
|
this.#announcer.announce(next);
|
|
return next;
|
|
});
|
|
}
|
|
if (isSegmentNavigationKey(e.key)) {
|
|
handleSegmentNavigation(e, this.root.getFieldNode());
|
|
}
|
|
}
|
|
props = $derived.by(() => {
|
|
const segmentValues = this.root.segmentValues;
|
|
if (!("dayPeriod" in segmentValues))
|
|
return;
|
|
const valueMin = 0;
|
|
const valueMax = 12;
|
|
const valueNow = segmentValues.dayPeriod === "AM" ? 0 : 12;
|
|
const valueText = segmentValues.dayPeriod ?? "AM";
|
|
return {
|
|
...this.root.sharedSegmentAttrs,
|
|
id: this.opts.id.current,
|
|
inputmode: "text",
|
|
"aria-label": "AM/PM",
|
|
"aria-valuemin": valueMin,
|
|
"aria-valuemax": valueMax,
|
|
"aria-valuenow": valueNow,
|
|
"aria-valuetext": valueText,
|
|
onkeydown: this.onkeydown,
|
|
onclick: this.root.handleSegmentClick,
|
|
...this.root.getBaseSegmentAttrs("dayPeriod", this.opts.id.current),
|
|
...this.attachment,
|
|
};
|
|
});
|
|
}
|
|
export class DateFieldLiteralSegmentState {
|
|
static create(opts) {
|
|
return new DateFieldLiteralSegmentState(opts, DateFieldRootContext.get());
|
|
}
|
|
opts;
|
|
root;
|
|
attachment;
|
|
constructor(opts, root) {
|
|
this.opts = opts;
|
|
this.root = root;
|
|
this.attachment = attachRef(opts.ref);
|
|
}
|
|
props = $derived.by(() => ({
|
|
id: this.opts.id.current,
|
|
"aria-hidden": boolToStrTrueOrUndef(true),
|
|
...this.root.getBaseSegmentAttrs("literal", this.opts.id.current),
|
|
...this.attachment,
|
|
}));
|
|
}
|
|
export class DateFieldTimeZoneSegmentState {
|
|
static create(opts) {
|
|
return new DateFieldTimeZoneSegmentState(opts, DateFieldRootContext.get());
|
|
}
|
|
opts;
|
|
root;
|
|
attachment;
|
|
constructor(opts, root) {
|
|
this.opts = opts;
|
|
this.root = root;
|
|
this.onkeydown = this.onkeydown.bind(this);
|
|
this.attachment = attachRef(opts.ref);
|
|
}
|
|
onkeydown(e) {
|
|
if (e.key !== kbd.TAB)
|
|
e.preventDefault();
|
|
if (this.root.disabled.current)
|
|
return;
|
|
if (isSegmentNavigationKey(e.key)) {
|
|
handleSegmentNavigation(e, this.root.getFieldNode());
|
|
}
|
|
}
|
|
props = $derived.by(() => ({
|
|
role: "textbox",
|
|
id: this.opts.id.current,
|
|
"aria-label": "timezone, ",
|
|
style: {
|
|
caretColor: "transparent",
|
|
},
|
|
onkeydown: this.onkeydown,
|
|
...this.root.getBaseSegmentAttrs("timeZoneName", this.opts.id.current),
|
|
"data-readonly": boolToEmptyStrOrUndef(true),
|
|
...this.attachment,
|
|
}));
|
|
}
|
|
export class DateFieldSegmentState {
|
|
static create(part, opts) {
|
|
const root = DateFieldRootContext.get();
|
|
switch (part) {
|
|
case "day":
|
|
return new DateFieldDaySegmentState(opts, root);
|
|
case "month":
|
|
return new DateFieldMonthSegmentState(opts, root);
|
|
case "year":
|
|
return new DateFieldYearSegmentState(opts, root);
|
|
case "hour":
|
|
return new DateFieldHourSegmentState(opts, root);
|
|
case "minute":
|
|
return new DateFieldMinuteSegmentState(opts, root);
|
|
case "second":
|
|
return new DateFieldSecondSegmentState(opts, root);
|
|
case "dayPeriod":
|
|
return new DateFieldDayPeriodSegmentState(opts, root);
|
|
case "literal":
|
|
return new DateFieldLiteralSegmentState(opts, root);
|
|
case "timeZoneName":
|
|
return new DateFieldTimeZoneSegmentState(opts, root);
|
|
}
|
|
}
|
|
}
|
|
function isAcceptableDayPeriodKey(key) {
|
|
return (isAcceptableSegmentKey(key) ||
|
|
key === kbd.A ||
|
|
key === kbd.P ||
|
|
key === kbd.a ||
|
|
key === kbd.p);
|
|
}
|
|
function isArrowUp(key) {
|
|
return key === kbd.ARROW_UP;
|
|
}
|
|
function isArrowDown(key) {
|
|
return key === kbd.ARROW_DOWN;
|
|
}
|
|
function isBackspace(key) {
|
|
return key === kbd.BACKSPACE;
|
|
}
|
|
function prependYearZeros(year) {
|
|
const digits = String(year).length;
|
|
const diff = 4 - digits;
|
|
return `${"0".repeat(diff)}${year}`;
|
|
}
|