headless-datetimepicker
Version:
React headless datepicker
824 lines (823 loc) • 21 kB
JavaScript
import Z, { createContext as $, useContext as P, useEffect as w, Fragment as V, isValidElement as ee, cloneElement as ne, createElement as re, forwardRef as _, useLayoutEffect as Y, useRef as D, useId as te, useMemo as L, useState as q, useCallback as oe, useReducer as se } from "react";
import { jsx as U } from "react/jsx-runtime";
import { offset as ie, flip as ce, shift as ue, useFloating as ae, autoUpdate as de } from "@floating-ui/react-dom";
import { isValid as le, format as fe, parse as pe, startOfMonth as ye, endOfMonth as he, startOfToday as me, startOfDay as ke, eachDayOfInterval as ge, addDays as N, isEqual as ve } from "date-fns";
function H(e, t) {
if (e in t) {
const r = t[e];
return typeof r == "function" ? r(e) : r;
}
throw new Error("Invalid match value");
}
function E(e, t) {
return (e % t + t) % t;
}
const we = (e, { type: t, payload: r }) => {
switch (t) {
case "action":
return S(e, r);
case "registerPicker":
return {
...e,
pickers: {
...e.pickers,
[r.id]: {
nestedLevel: r.nestedLevel,
defaultType: r.defaultType,
type: r.defaultType,
attach: void 0,
isOpen: r.defaultOpen,
alwaysOpen: r.alwaysOpen
}
}
};
case "unregisterPicker": {
const { [r]: n, ...o } = e.pickers;
return { ...e, pickers: o };
}
case "select": {
const n = r.action ? S(e, {
action: r.action,
pickerId: r.pickerId
}) : { ...e };
let o = null;
switch (r.item.type) {
case "day": {
o = new Date(r.item.value), o.setHours(e.hour, e.minute);
break;
}
case "month": {
n.month = r.item.value;
break;
}
case "year": {
n.year = r.item.value;
break;
}
case "hour": {
o = e.valueRef.current ? new Date(e.valueRef.current) : /* @__PURE__ */ new Date(), o.setHours(r.item.value), n.hour = r.item.value;
break;
}
case "minute": {
o = e.valueRef.current ? new Date(e.valueRef.current) : /* @__PURE__ */ new Date(), o.setMinutes(r.item.value), n.minute = r.item.value;
break;
}
default:
return e;
}
return o && n.onChange(o), n;
}
case "defaultChanged":
return {
...e,
...r
};
case "externalValueChanged": {
const n = e.config.toDateParts(r);
return {
...e,
year: n.year,
month: n.month,
hour: r.getHours(),
minute: r.getMinutes()
};
}
default:
throw new Error("Invalid action " + t);
}
};
function S(e, t) {
let r = t.action, n = "";
const o = t.action.match(
/^(open|close|next|prev|showYear|showMonth|showDay|toggleYear|toggleMonth|toggleDay|toggle)(.*)$/
);
if (o && (r = o[1], n = o[2], n === "" && (n = t.pickerId || Object.keys(e.pickers).reverse()[0], n === void 0)))
throw new Error("There is no Picker in the current Provider");
switch (r) {
case "open":
return {
...e,
pickers: {
...e.pickers,
[n]: {
...e.pickers[n],
attach: t.ref,
isOpen: !0
}
}
};
case "close":
return {
...e,
pickers: {
...e.pickers,
[n]: {
...e.pickers[n],
isOpen: !1,
type: e.pickers[n].defaultType
}
}
};
case "toggle":
return {
...e,
pickers: {
...e.pickers,
[n]: {
...e.pickers[n],
attach: t.ref,
isOpen: !e.pickers[n].isOpen,
type: e.pickers[n].defaultType
}
}
};
case "next": {
if (!e.pickers[n].type)
return e;
const { month: i, year: s } = e;
return H(e.pickers[n].type, {
hour: () => e,
minute: () => e,
day: () => ({
...e,
year: i === 12 ? s + 1 : s,
month: i % 12 + 1
}),
month: () => ({
...e,
year: s + 1,
month: i
}),
year: () => ({
...e,
year: s + 1,
month: i
})
});
}
case "prev": {
if (!e.pickers[n].type)
return e;
const { month: i, year: s } = e;
return H(e.pickers[n].type, {
hour: () => e,
minute: () => e,
day: () => ({
...e,
year: i === 1 ? s - 1 : s,
month: E(i - 2, 12) + 1
}),
month: () => ({
...e,
year: s - 1,
month: i
}),
year: () => ({
...e,
year: s - 1,
month: i
})
});
}
case "showYear":
return {
...e,
pickers: {
...e.pickers,
[n]: {
...e.pickers[n],
type: "year"
}
}
};
case "toggleYear":
return {
...e,
pickers: {
...e.pickers,
[n]: {
...e.pickers[n],
type: e.pickers[n].type === "year" ? e.pickers[n].defaultType : "year"
}
}
};
case "showMonth":
return {
...e,
pickers: {
...e.pickers,
[n]: {
...e.pickers[n],
type: "month"
}
}
};
case "toggleMonth":
return {
...e,
pickers: {
...e.pickers,
[n]: {
...e.pickers[n],
type: e.pickers[n].type === "month" ? e.pickers[n].defaultType : "month"
}
}
};
case "showDay":
return {
...e,
pickers: {
...e.pickers,
[n]: {
...e.pickers[n],
type: "day"
}
}
};
case "toggleDay":
return {
...e,
pickers: {
...e.pickers,
[n]: {
...e.pickers[n],
type: e.pickers[n].type === "day" ? e.pickers[n].defaultType : "day"
}
}
};
case "today": {
const i = /* @__PURE__ */ new Date();
i.setHours(e.hour, e.minute), e.onChange(i);
const s = e.config.toDateParts(i);
return {
...e,
year: s.year,
month: s.month
};
}
case "todayHour": {
const i = /* @__PURE__ */ new Date();
e.onChange(i);
const s = e.config.toDateParts(i);
return {
...e,
year: s.year,
month: s.month
};
}
default:
throw new Error("Invalid action " + t.action);
}
}
const G = $(null);
function De() {
const e = P(G);
if (!e)
throw new Error("You need to use component inside Datepicker");
return e;
}
function O() {
const e = De();
return {
...e,
slot: B(e.state)
};
}
function B(e) {
return {
pickers: e.pickers,
disabled: e.disabled,
value: e.valueRef.current,
month: e.month,
monthName: e.config.monthNames[e.month - 1],
year: e.year,
hour: e.hour,
minute: e.minute
};
}
const M = (e, t) => {
w(() => {
t && (typeof t == "function" ? t(e.current) : t.current = e.current);
});
};
function j(...e) {
return e.filter(Boolean).join(" ");
}
function T(e, t, r = {}, n, o, i = {}) {
if ((i == null ? void 0 : i.visible) === !1 && i.hideOnClose !== !0)
return null;
const { as: s, children: c, ...u } = R(t, e), y = s || n, l = typeof c == "function" ? c(r) : c;
if (typeof u.className == "function" && (u.className = u.className(r)), y === V && Object.keys(u).length > 0) {
if (!ee(l) || Array.isArray(l) && l.length > 1)
throw new Error(
[
'Passing props on "Fragment"!',
"",
'The current component is rendering a "Fragment".',
"However we need to passthrough the following props:",
Object.keys(u).map((h) => ` - ${h}`).join(`
`),
"",
"You can apply a few solutions:",
[
'Add an `as="..."` prop, to ensure that we render an actual element instead of a "Fragment".',
"Render a single element as the child so that we can forward the props onto that element."
].map((h) => ` - ${h}`).join(`
`)
].join(`
`)
);
const { childClassName: a, ...m } = l.props, d = typeof a == "function" ? (...h) => j(a(...h), u.className) : j(a, u.className), g = d ? { className: d, ref: o } : { ref: o };
return ne(
l,
Object.assign(
{},
// Filter out undefined values so that they don't override the existing values
R(m, u),
g
)
);
}
return re(y, { ...u, ref: o }, l);
}
function R(...e) {
if (e.length === 0)
return {};
if (e.length === 1)
return e[0];
const t = {}, r = {};
for (const n of e)
for (const o in n)
o.startsWith("on") && typeof n[o] == "function" ? (r[o] ?? (r[o] = []), r[o].push(n[o])) : t[o] = n[o];
if (t.disabled || t["aria-disabled"])
return Object.assign(
t,
// Set all event listeners that we collected to `undefined`. This is
// important because of the `cloneElement` from above, which merges the
// existing and new props, they don't just override therefore we have to
// explicitly nullify them.
Object.fromEntries(
Object.keys(r).map((n) => [n, void 0])
)
);
for (const n in r)
Object.assign(t, {
[n](o, ...i) {
const s = r[n];
for (const c of s) {
if ((o instanceof Event || (o == null ? void 0 : o.nativeEvent) instanceof Event) && o.defaultPrevented)
return;
c(o, ...i);
}
}
});
return t;
}
function C(e) {
return _(e);
}
const J = typeof window < "u" ? Y : w;
function be(e, t, r, n) {
const o = D(t);
J(() => {
o.current = t;
}, [t]), w(() => {
const i = (r == null ? void 0 : r.current) ?? window;
if (!(i && i.addEventListener))
return;
const s = (c) => o.current(c);
return i.addEventListener(e, s, n), () => {
i.removeEventListener(e, s, n);
};
}, [e, r, n]);
}
function Te(e, t) {
be("mousedown", (r) => {
(Array.isArray(e) ? e : [e]).some((n) => {
if (n === void 0)
return !1;
const o = n == null ? void 0 : n.current;
if (!o || o.contains(r.target))
return !0;
}) || t(r);
});
}
const Ee = "div", Ae = [
ie(10),
ce({ fallbackAxisSideDirection: "end", crossAxis: !1 }),
ue()
], x = $({ nestedLevel: 0 }), xe = C(
({
alwaysOpen: e,
hideOnClose: t,
middleware: r = Ae,
attachTo: n,
style: o,
defaultType: i,
defaultOpen: s = !1,
disableClickOutside: c = !1,
id: u,
...y
}, l) => {
const { nestedLevel: a } = P(x), m = te(), d = u || m, { state: g, slot: h, dispatch: v } = O(), p = D(i), b = D(s);
w(() => (v({
type: "registerPicker",
payload: {
id: d,
nestedLevel: a + 1,
defaultType: p.current,
defaultOpen: b.current,
alwaysOpen: e
}
}), () => v({ type: "unregisterPicker", payload: d })), [v, d, a, e]);
const f = g.pickers[d], k = n === !1 ? void 0 : n !== void 0 ? n : e || f == null ? void 0 : f.attach, F = e || (f == null ? void 0 : f.isOpen) || !1, { refs: I, floatingStyles: z } = ae({
open: F,
elements: {
reference: k ? k.current : null
},
middleware: r,
whileElementsMounted: de
});
M(I.floating, l);
const Q = () => {
c !== !0 && (f != null && f.isOpen) && v({
type: "action",
payload: { action: `close${d}` }
});
};
Te([I.floating, k], Q);
const X = {
style: {
...o,
...k != null && k.current ? z : {}
}
};
return /* @__PURE__ */ U(
x.Provider,
{
value: L(
() => ({
nestedLevel: a + 1,
id: d,
defaultType: p.current
}),
[a, d]
),
children: T(X, y, h, Ee, I.setFloating, {
visible: F,
hideOnClose: t
})
}
);
}
), Pe = "button", Oe = C(
({ action: e, ...t }, r) => {
const { id: n } = P(x), o = D(null);
M(o, r);
const { slot: i, dispatch: s } = O();
return T({
onClick: () => s({
type: "action",
payload: { action: e, ref: o, pickerId: n }
})
}, t, i, Pe, o);
}
);
function W() {
const [e] = q(Ce);
return w(() => () => e.dispose(), [e]), e;
}
function Ce() {
const e = [], t = {
addEventListener(r, n, o, i) {
return r.addEventListener(n, o, i), t.add(
() => r.removeEventListener(n, o, i)
);
},
requestAnimationFrame(...r) {
const n = requestAnimationFrame(...r);
return t.add(() => cancelAnimationFrame(n));
},
nextFrame(...r) {
return t.requestAnimationFrame(() => t.requestAnimationFrame(...r));
},
add(r) {
return e.push(r), () => {
const n = e.indexOf(r);
if (n >= 0)
for (const o of e.splice(n, 1))
o();
};
},
dispose() {
for (const r of e.splice(0))
r();
}
};
return t;
}
const A = function(t) {
const r = D(t);
return J(() => {
r.current = t;
}, [t]), Z.useCallback((...n) => r.current(...n), [r]);
}, Fe = "input", Ie = C(
({
format: e = "yyyy/MM/dd",
parse: t,
type: r,
...n
}, o) => {
const i = D(null);
M(i, o);
const { state: s, slot: c, dispatch: u } = O(), y = oe(
(f) => typeof e == "function" ? e(f) : s.config.format(f, e),
[e, s.config]
), [l, a] = q(
void 0
), m = L(
() => y(c.value),
[c.value, y]
), d = W(), g = A(
() => d.nextFrame(
() => u({
type: "action",
payload: { action: "open", ref: i }
})
)
), h = A((f) => a(f.target.value)), v = A((f) => {
let k = null;
if (f.target.value)
try {
k = typeof e == "function" ? t(f.target.value, c.value) : s.config.parse(f.target.value, e, c.value);
} catch {
}
k !== null && le(k) && s.onChange(k), d.nextFrame(() => a(void 0));
}), p = typeof e == "function" && typeof t != "function", b = {
type: r || "text",
readOnly: p,
disabled: s.disabled,
value: l !== void 0 ? l : m,
onFocus: g,
onChange: p ? void 0 : h,
onBlur: p ? void 0 : v
};
return T(b, n, c, Fe, i);
}
), Le = "button", K = "data-calendar-item-id", Me = C(
({ item: e, action: t, ...r }, n) => {
const { id: o } = P(x), { state: i, slot: s, dispatch: c } = O(), u = {
[K]: e.type + "-" + e.text,
onClick: "isHeader" in e && e.isHeader || i.disabled ? void 0 : () => {
c({
type: "select",
payload: { item: e, pickerId: o, action: t }
});
}
};
return T(u, r, s, Le, n);
}
);
function Ne(e, t, r) {
Y(() => {
if (e && r && t) {
const n = document.querySelector(
`[${K}="${t}-${r}"]`
);
n && n.scrollIntoView({ block: "nearest" });
}
}, [t, r, e]);
}
const He = "div", Se = C(
({
type: e,
disableAutoScroll: t,
...r
}, n) => {
const { id: o, defaultType: i } = P(x), { state: s } = O(), c = o ? s.pickers[o] : void 0, u = e || (c == null ? void 0 : c.type) || i;
if (u === void 0)
throw new Error(
"No type provided, You need either need set the type to Items or set the defaultType to Picker component"
);
const y = s.valueRef.current, l = s.filterDate, a = L(
() => u === "hour" || u === "minute" ? s.config[u + "s"]({
type: u,
hour: s.hour,
minute: s.minute
}) : s.config[u + "s"]({
type: u,
year: s.year,
month: s.month,
value: y,
startOfWeek: s.startOfWeek
}).map(
(d) => d.type === "day" && !d.isHeader && !l(d.value) ? { ...d, isDisabled: !0 } : d
),
[
u,
y,
s.config,
s.month,
s.year,
s.hour,
s.minute,
s.startOfWeek,
l
]
);
return Ne(
t !== !0 && c !== void 0 && (c.alwaysOpen === !0 || c.isOpen) && ["year", "hour", "minute"].includes(u),
u,
u !== "day" ? s[u] : void 0
), T(
{},
r,
{
items: a,
type: u,
...s
},
He,
n
);
}
), je = {
dayNames: [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
],
monthNames: [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
],
format: function(e, t) {
return e ? fe(e, t) : "";
},
parse: function(e, t, r) {
return pe(e, t, r || /* @__PURE__ */ new Date());
},
toDateParts: function(e) {
return new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "numeric",
day: "numeric"
}).formatToParts(e).reduce((t, r) => (r.type !== "literal" && (t[r.type] = +r.value), t), {});
},
years: function({ type: e, year: t }) {
const r = (/* @__PURE__ */ new Date()).getFullYear();
return [...Array(200).keys()].map((n) => ({
type: e,
key: e + n,
isToday: r === n + 1900,
isSelected: t === n + 1900,
isHeader: !1,
isDisabled: !1,
value: n + 1900,
text: n + 1900 + ""
}));
},
months: function({ type: e, month: t }) {
const r = (/* @__PURE__ */ new Date()).getMonth();
return [...this.monthNames.keys()].map((n) => ({
type: e,
key: e + n,
isToday: r === n,
isSelected: t === n + 1,
isHeader: !1,
isDisabled: !1,
value: n + 1,
text: this.monthNames[n]
}));
},
days: function({ type: e, month: t, startOfWeek: r, year: n, value: o }) {
const i = new Date(n, t - 1, 1), s = ye(i), c = he(i), u = E(r - 1, 7), y = me().getTime(), l = o ? ke(o).getTime() : 0;
return this.dayNames.map((a, m) => {
const d = E(r + m, 7);
return {
type: e,
key: "weekday" + d,
isToday: !1,
isSelected: !1,
isHeader: !0,
isDisabled: !1,
value: m,
text: this.dayNames[d]
};
}).concat(
ge({
start: N(s, -E(s.getDay() - r, 7)),
end: N(c, E(u - c.getDay(), 7))
}).map((a) => ({
type: e,
key: a.toString(),
isToday: y === a.getTime(),
isSelected: l === a.getTime(),
isHeader: !1,
isInCurrentMonth: a >= s && a <= c,
isDisabled: a < s || a > c,
value: a,
text: a.getDate() + ""
}))
);
},
hours: function({ type: e, hour: t }) {
return [...Array(24).keys()].map((r) => ({
type: e,
key: r,
value: r,
text: r + "",
isToday: !1,
isSelected: t === r,
isHeader: !1,
isDisabled: !1
}));
},
minutes: function({ type: e, minute: t }) {
return [...Array(60).keys()].map((r) => ({
type: e,
key: r,
value: r,
text: r + "",
isToday: !1,
isSelected: t === r,
isHeader: !1,
isDisabled: !1
}));
}
}, Re = V, $e = _(
({
defaultValue: e,
value: t,
onChange: r,
disabledKeyboardNavigation: n,
disabled: o = !1,
config: i = je,
startOfWeek: s = 0,
filterDate: c = () => !0,
...u
}, y) => {
const l = D(t || e || null), a = W(), m = A((p) => {
Ve(l.current, p) || p && !c(p) || a.nextFrame(() => {
l.current = p, r == null || r(l.current), h({
type: "externalValueChanged",
payload: p || /* @__PURE__ */ new Date()
});
});
}), d = A(c), [g, h] = se(we, null, () => {
const p = l.current || /* @__PURE__ */ new Date(), b = i.toDateParts(p);
return {
config: i,
disabled: o,
year: b.year,
month: b.month,
hour: p.getHours(),
minute: p.getMinutes(),
calendarOpen: !1,
hourOpen: !1,
valueRef: l,
startOfWeek: s,
onChange: m,
filterDate: d,
pickers: {}
};
});
w(() => {
m(t || null);
}, [t, m]), w(() => {
h({ type: "defaultChanged", payload: { startOfWeek: s } });
}, [s]), w(() => {
h({ type: "defaultChanged", payload: { disabled: o } });
}, [o]);
const v = {};
return /* @__PURE__ */ U(G.Provider, { value: { state: g, dispatch: h }, children: T(v, u, B(g), Re, y) });
}
);
function Ve(e, t) {
return e === t || e !== null && t !== null && ve(e, t);
}
const Ge = Object.assign($e, {
Picker: xe,
Input: Ie,
Button: Oe,
Items: Se,
Item: Me
});
export {
Ge as Datepicker,
je as config
};