bits-ui
Version:
The headless components for Svelte.
203 lines (202 loc) • 7.88 kB
JavaScript
import { box, onDestroyEffect, attachRef, DOMContext, } from "svelte-toolbelt";
import { Context, watch } from "runed";
import { DateFieldInputState, DateFieldRootState } from "../date-field/date-field.svelte.js";
import { useId } from "../../internal/use-id.js";
import { createBitsAttrs, getDataDisabled, getDataInvalid } from "../../internal/attrs.js";
import { createFormatter } from "../../internal/date-time/formatter.js";
import { removeDescriptionElement } from "../../internal/date-time/field/helpers.js";
import { isBefore } from "../../internal/date-time/utils.js";
import { getFirstSegment } from "../../internal/date-time/field/segments.js";
export const dateRangeFieldAttrs = createBitsAttrs({
component: "date-range-field",
parts: ["root", "label"],
});
export const DateRangeFieldRootContext = new Context("DateRangeField.Root");
export class DateRangeFieldRootState {
static create(opts) {
return DateRangeFieldRootContext.set(new DateRangeFieldRootState(opts));
}
opts;
startFieldState = undefined;
endFieldState = undefined;
descriptionId = useId();
formatter;
fieldNode = $state(null);
labelNode = $state(null);
descriptionNode = $state(null);
startValueComplete = $derived.by(() => this.opts.startValue.current !== undefined);
endValueComplete = $derived.by(() => this.opts.endValue.current !== undefined);
rangeComplete = $derived(this.startValueComplete && this.endValueComplete);
domContext;
attachment;
constructor(opts) {
this.opts = opts;
this.formatter = createFormatter({
initialLocale: this.opts.locale.current,
monthFormat: box.with(() => "long"),
yearFormat: box.with(() => "numeric"),
});
this.domContext = new DOMContext(this.opts.ref);
this.attachment = attachRef(this.opts.ref, (v) => (this.fieldNode = v));
onDestroyEffect(() => {
removeDescriptionElement(this.descriptionId, this.domContext.getDocument());
});
$effect(() => {
if (this.formatter.getLocale() === this.opts.locale.current)
return;
this.formatter.setLocale(this.opts.locale.current);
});
/**
* 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;
}
});
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;
}
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;
}
});
}
validationStatus = $derived.by(() => {
const value = this.opts.value.current;
if (value === undefined)
return false;
if (value.start === undefined || value.end === undefined)
return false;
const msg = this.opts.validate.current?.({
start: value.start,
end: value.end,
});
if (msg) {
return {
reason: "custom",
message: msg,
};
}
const minValue = this.opts.minValue.current;
if (minValue && value.start && isBefore(value.start, minValue)) {
return {
reason: "min",
};
}
const maxValue = this.opts.maxValue.current;
if ((maxValue && value.end && isBefore(maxValue, value.end)) ||
(maxValue && value.start && isBefore(maxValue, value.start))) {
return {
reason: "max",
};
}
return false;
});
isInvalid = $derived.by(() => {
if (this.validationStatus === false)
return false;
return true;
});
#updateValue(cb) {
const value = this.opts.value.current;
const newValue = cb(value);
this.opts.value.current = newValue;
}
props = $derived.by(() => ({
id: this.opts.id.current,
role: "group",
[dateRangeFieldAttrs.root]: "",
"data-invalid": getDataInvalid(this.isInvalid),
...this.attachment,
}));
}
export class DateRangeFieldLabelState {
static create(opts) {
return new DateRangeFieldLabelState(opts, DateRangeFieldRootContext.get());
}
opts;
root;
attachment;
constructor(opts, root) {
this.opts = opts;
this.root = root;
this.attachment = attachRef(this.opts.ref, (v) => (this.root.labelNode = v));
}
#onclick = () => {
if (this.root.opts.disabled.current)
return;
const firstSegment = getFirstSegment(this.root.fieldNode);
if (!firstSegment)
return;
firstSegment.focus();
};
props = $derived.by(() => ({
id: this.opts.id.current,
"data-invalid": getDataInvalid(this.root.isInvalid),
"data-disabled": getDataDisabled(this.root.opts.disabled.current),
[dateRangeFieldAttrs.label]: "",
onclick: this.#onclick,
...this.attachment,
}));
}
export class DateRangeFieldInputState {
static create(opts, type) {
const root = DateRangeFieldRootContext.get();
const fieldState = DateFieldRootState.create({
value: type === "start" ? root.opts.startValue : root.opts.endValue,
disabled: root.opts.disabled,
readonly: root.opts.readonly,
readonlySegments: root.opts.readonlySegments,
validate: box.with(() => undefined),
minValue: root.opts.minValue,
maxValue: root.opts.maxValue,
hourCycle: root.opts.hourCycle,
locale: root.opts.locale,
hideTimeZone: root.opts.hideTimeZone,
required: root.opts.required,
granularity: root.opts.granularity,
placeholder: root.opts.placeholder,
onInvalid: root.opts.onInvalid,
errorMessageId: root.opts.errorMessageId,
isInvalidProp: box.with(() => root.isInvalid),
}, root);
return new DateFieldInputState({ name: opts.name, id: opts.id, ref: opts.ref }, fieldState);
}
}