UNPKG

headless-datetimepicker

Version:
824 lines (823 loc) 21 kB
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 };