Add Datepicker component for date selection
This commit is contained in:
252
src/components/Datepicker.js
Normal file
252
src/components/Datepicker.js
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
import { $, $html, $if } from "sigpro";
|
||||||
|
import { val } from "../core/utils.js";
|
||||||
|
import {
|
||||||
|
iconCalendar,
|
||||||
|
iconLeft,
|
||||||
|
iconRight,
|
||||||
|
iconLLeft,
|
||||||
|
iconRRight
|
||||||
|
} from "../core/icons.js";
|
||||||
|
import { Input } from "./Input.js";
|
||||||
|
|
||||||
|
/** DATEPICKER */
|
||||||
|
export const Datepicker = (props) => {
|
||||||
|
const { value, range, label, placeholder, hour = false, ...rest } = props;
|
||||||
|
|
||||||
|
const isOpen = $(false);
|
||||||
|
const internalDate = $(new Date());
|
||||||
|
const hoverDate = $(null);
|
||||||
|
const startHour = $(0);
|
||||||
|
const endHour = $(0);
|
||||||
|
const isRangeMode = () => val(range) === true;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const todayStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
||||||
|
|
||||||
|
const formatDate = (d) => {
|
||||||
|
const year = d.getFullYear();
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(d.getDate()).padStart(2, "0");
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectDate = (date) => {
|
||||||
|
const dateStr = formatDate(date);
|
||||||
|
const current = val(value);
|
||||||
|
|
||||||
|
if (isRangeMode()) {
|
||||||
|
if (!current?.start || (current.start && current.end)) {
|
||||||
|
if (typeof value === "function") {
|
||||||
|
value({
|
||||||
|
start: dateStr,
|
||||||
|
end: null,
|
||||||
|
...(hour && { startHour: startHour() }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const start = current.start;
|
||||||
|
if (typeof value === "function") {
|
||||||
|
const newValue = dateStr < start ? { start: dateStr, end: start } : { start, end: dateStr };
|
||||||
|
if (hour) {
|
||||||
|
newValue.startHour = current.startHour || startHour();
|
||||||
|
newValue.endHour = current.endHour || endHour();
|
||||||
|
}
|
||||||
|
value(newValue);
|
||||||
|
}
|
||||||
|
isOpen(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (typeof value === "function") {
|
||||||
|
value(hour ? `${dateStr}T${String(startHour()).padStart(2, "0")}:00:00` : dateStr);
|
||||||
|
}
|
||||||
|
isOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayValue = $(() => {
|
||||||
|
const v = val(value);
|
||||||
|
if (!v) return "";
|
||||||
|
if (typeof v === "string") {
|
||||||
|
if (hour && v.includes("T")) return v.replace("T", " ");
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
if (v.start && v.end) {
|
||||||
|
const startStr = hour && v.startHour ? `${v.start} ${String(v.startHour).padStart(2, "0")}:00` : v.start;
|
||||||
|
const endStr = hour && v.endHour ? `${v.end} ${String(v.endHour).padStart(2, "0")}:00` : v.end;
|
||||||
|
return `${startStr} - ${endStr}`;
|
||||||
|
}
|
||||||
|
if (v.start) {
|
||||||
|
const startStr = hour && v.startHour ? `${v.start} ${String(v.startHour).padStart(2, "0")}:00` : v.start;
|
||||||
|
return `${startStr}...`;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
|
||||||
|
const move = (m) => {
|
||||||
|
const d = internalDate();
|
||||||
|
internalDate(new Date(d.getFullYear(), d.getMonth() + m, 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveYear = (y) => {
|
||||||
|
const d = internalDate();
|
||||||
|
internalDate(new Date(d.getFullYear() + y, d.getMonth(), 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const HourSlider = ({ value: hVal, onChange }) => {
|
||||||
|
return $html("div", { class: "flex-1" }, [
|
||||||
|
$html("div", { class: "flex gap-2 items-center" }, [
|
||||||
|
$html("input", {
|
||||||
|
type: "range",
|
||||||
|
min: 0,
|
||||||
|
max: 23,
|
||||||
|
value: hVal,
|
||||||
|
class: "range range-xs flex-1",
|
||||||
|
oninput: (e) => {
|
||||||
|
const newHour = parseInt(e.target.value);
|
||||||
|
onChange(newHour);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
$html("span", { class: "text-sm font-mono min-w-[48px] text-center" },
|
||||||
|
() => String(val(hVal)).padStart(2, "0") + ":00"
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return $html("div", { class: "relative w-full" }, [
|
||||||
|
Input({
|
||||||
|
label,
|
||||||
|
placeholder: placeholder || (isRangeMode() ? "Seleccionar rango..." : "Seleccionar fecha..."),
|
||||||
|
value: displayValue,
|
||||||
|
readonly: true,
|
||||||
|
icon: $html("img", { src: iconCalendar, class: "opacity-40" }),
|
||||||
|
onclick: (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
isOpen(!isOpen());
|
||||||
|
},
|
||||||
|
...rest,
|
||||||
|
}),
|
||||||
|
|
||||||
|
$if(isOpen, () =>
|
||||||
|
$html(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
class: "absolute left-0 mt-2 p-4 bg-base-100 border border-base-300 shadow-2xl rounded-box z-[100] w-80 select-none",
|
||||||
|
onclick: (e) => e.stopPropagation(),
|
||||||
|
},
|
||||||
|
[
|
||||||
|
$html("div", { class: "flex justify-between items-center mb-4 gap-1" }, [
|
||||||
|
$html("div", { class: "flex gap-0.5" }, [
|
||||||
|
$html("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => moveYear(-1) },
|
||||||
|
$html("img", { src: iconLLeft, class: "opacity-40" })
|
||||||
|
),
|
||||||
|
$html("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => move(-1) },
|
||||||
|
$html("img", { src: iconLeft, class: "opacity-40" })
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
$html("span", { class: "font-bold uppercase flex-1 text-center" }, [
|
||||||
|
() => internalDate().toLocaleString("es-ES", { month: "short", year: "numeric" }),
|
||||||
|
]),
|
||||||
|
$html("div", { class: "flex gap-0.5" }, [
|
||||||
|
$html("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => move(1) },
|
||||||
|
$html("img", { src: iconRight, class: "opacity-40" })
|
||||||
|
),
|
||||||
|
$html("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => moveYear(1) },
|
||||||
|
$html("img", { src: iconRRight, class: "opacity-40" })
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
|
||||||
|
$html("div", { class: "grid grid-cols-7 gap-1", onmouseleave: () => hoverDate(null) }, [
|
||||||
|
...["L", "M", "X", "J", "V", "S", "D"].map((d) => $html("div", { class: "text-[10px] opacity-40 font-bold text-center" }, d)),
|
||||||
|
() => {
|
||||||
|
const d = internalDate();
|
||||||
|
const year = d.getFullYear();
|
||||||
|
const month = d.getMonth();
|
||||||
|
const firstDay = new Date(year, month, 1).getDay();
|
||||||
|
const offset = firstDay === 0 ? 6 : firstDay - 1;
|
||||||
|
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||||
|
|
||||||
|
const nodes = [];
|
||||||
|
for (let i = 0; i < offset; i++) nodes.push($html("div"));
|
||||||
|
|
||||||
|
for (let i = 1; i <= daysInMonth; i++) {
|
||||||
|
const date = new Date(year, month, i);
|
||||||
|
const dStr = formatDate(date);
|
||||||
|
|
||||||
|
nodes.push(
|
||||||
|
$html(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
class: () => {
|
||||||
|
const v = val(value);
|
||||||
|
const h = hoverDate();
|
||||||
|
const isStart = typeof v === "string" ? v.split("T")[0] === dStr : v?.start === dStr;
|
||||||
|
const isEnd = v?.end === dStr;
|
||||||
|
let inRange = false;
|
||||||
|
|
||||||
|
if (isRangeMode() && v?.start) {
|
||||||
|
const start = v.start;
|
||||||
|
if (!v.end && h) {
|
||||||
|
inRange = (dStr > start && dStr <= h) || (dStr < start && dStr >= h);
|
||||||
|
} else if (v.end) {
|
||||||
|
inRange = dStr > start && dStr < v.end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = "btn btn-xs p-0 aspect-square min-h-0 h-auto font-normal relative";
|
||||||
|
const state = isStart || isEnd ? "btn-primary z-10" : inRange ? "bg-primary/20 border-none rounded-none" : "btn-ghost";
|
||||||
|
const today = dStr === todayStr ? "ring-1 ring-primary ring-inset font-black text-primary" : "";
|
||||||
|
|
||||||
|
return `${base} ${state} ${today}`;
|
||||||
|
},
|
||||||
|
onmouseenter: () => { if (isRangeMode()) hoverDate(dStr); },
|
||||||
|
onclick: () => selectDate(date),
|
||||||
|
},
|
||||||
|
[i.toString()],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
|
||||||
|
hour ? $html("div", { class: "mt-3 pt-2 border-t border-base-300" }, [
|
||||||
|
isRangeMode()
|
||||||
|
? $html("div", { class: "flex gap-4" }, [
|
||||||
|
HourSlider({
|
||||||
|
value: startHour,
|
||||||
|
onChange: (newHour) => {
|
||||||
|
startHour(newHour);
|
||||||
|
const currentVal = val(value);
|
||||||
|
if (currentVal?.start) value({ ...currentVal, startHour: newHour });
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
HourSlider({
|
||||||
|
value: endHour,
|
||||||
|
onChange: (newHour) => {
|
||||||
|
endHour(newHour);
|
||||||
|
const currentVal = val(value);
|
||||||
|
if (currentVal?.end) value({ ...currentVal, endHour: newHour });
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
: HourSlider({
|
||||||
|
value: startHour,
|
||||||
|
onChange: (newHour) => {
|
||||||
|
startHour(newHour);
|
||||||
|
const currentVal = val(value);
|
||||||
|
if (currentVal && typeof currentVal === "string" && currentVal.includes("-")) {
|
||||||
|
value(currentVal.split("T")[0] + "T" + String(newHour).padStart(2, "0") + ":00:00");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]) : null,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
$if(isOpen, () => $html("div", { class: "fixed inset-0 z-[90]", onclick: () => isOpen(false) })),
|
||||||
|
]);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user