import { zonedWallToUtc, utcToZonedWall, tzShort } from "../tz.js";
const MONTHS = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December",
];
const wallFromZoned = ({ y, mo, d, h, mi }) => new Date(y, mo, d, h, mi);
const WD = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
const pad = (n) => String(n).padStart(2, "0");
const sameDay = (a, b) =>
a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate();
function toRaw(d) {
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
function toDisplay(d) {
return `${pad(d.getDate())} ${MONTHS[d.getMonth()].slice(0, 3)} ${d.getFullYear()}, ${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
function parseRaw(s) {
if (!s) return null;
const d = new Date(s);
return isNaN(d.getTime()) ? null : d;
}
export function attachDatePicker(input) {
let popup = null;
let view = parseRaw(input.dataset.value) || new Date();
let pending = parseRaw(input.dataset.value);
const isOpen = () => popup !== null;
function commit(date) {
if (date) {
const utc = zonedWallToUtc(
date.getFullYear(), date.getMonth(), date.getDate(),
date.getHours(), date.getMinutes(),
);
input.dataset.value = utc.toISOString();
input.value = toDisplay(date);
} else {
input.dataset.value = "";
input.value = "";
}
input.dispatchEvent(new Event("change", { bubbles: true }));
}
function timeFromInputs() {
if (!popup) return { h: 0, m: 0 };
const h = Math.min(23, Math.max(0, parseInt(popup.querySelector(".dp-h").value, 10) || 0));
const m = Math.min(59, Math.max(0, parseInt(popup.querySelector(".dp-m").value, 10) || 0));
return { h, m };
}
function dayCell(d, muted, today) {
const cls = ["dp-day"];
if (muted) cls.push("dp-muted");
if (pending && sameDay(d, pending)) cls.push("dp-sel");
if (sameDay(d, today)) cls.push("dp-today-c");
return `<button type="button" class="${cls.join(" ")}" data-d="${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}">${d.getDate()}</button>`;
}
function render() {
const y = view.getFullYear();
const m = view.getMonth();
const startWd = new Date(y, m, 1).getDay();
const daysInMonth = new Date(y, m + 1, 0).getDate();
const today = new Date();
let cells = "";
for (let i = 0; i < startWd; i++) {
cells += dayCell(new Date(y, m, 1 - (startWd - i)), true, today);
}
for (let day = 1; day <= daysInMonth; day++) {
cells += dayCell(new Date(y, m, day), false, today);
}
const filled = startWd + daysInMonth;
for (let i = 1; i <= 42 - filled; i++) {
cells += dayCell(new Date(y, m + 1, i), true, today);
}
const t = pending || new Date();
popup.innerHTML = `
<div class="dp-head">
<button type="button" class="dp-nav" data-nav="-1" aria-label="Previous month">‹</button>
<span class="dp-title">${MONTHS[m]} ${y}</span>
<button type="button" class="dp-nav" data-nav="1" aria-label="Next month">›</button>
</div>
<div class="dp-grid dp-wd">${WD.map((w) => `<span class="dp-wdc">${w}</span>`).join("")}</div>
<div class="dp-grid dp-days">${cells}</div>
<div class="dp-time">
<span class="dp-time-lbl">Time <span class="dp-tz">· ${tzShort()}</span></span>
<input class="dp-h" type="number" min="0" max="23" value="${pad(t.getHours())}" aria-label="Hour" />
<span class="dp-colon">:</span>
<input class="dp-m" type="number" min="0" max="59" value="${pad(t.getMinutes())}" aria-label="Minute" />
</div>
<div class="dp-foot">
<button type="button" class="dp-clear">Clear</button>
<div class="dp-foot-r">
<button type="button" class="dp-today">Today</button>
<button type="button" class="dp-done">Done</button>
</div>
</div>`;
popup.querySelectorAll(".dp-nav").forEach((b) => {
b.onclick = () => { view = new Date(y, m + Number(b.dataset.nav), 1); render(); };
});
popup.querySelectorAll(".dp-day").forEach((b) => {
b.onclick = () => {
const [yy, mm, dd] = b.dataset.d.split("-").map(Number);
const { h, m: mi } = timeFromInputs();
pending = new Date(yy, mm - 1, dd, h, mi);
view = new Date(yy, mm - 1, 1);
render();
};
});
const syncTime = () => {
const { h, m: mi } = timeFromInputs();
pending = pending || new Date();
pending.setHours(h, mi, 0, 0);
};
popup.querySelector(".dp-h").onchange = syncTime;
popup.querySelector(".dp-m").onchange = syncTime;
popup.querySelector(".dp-clear").onclick = () => { pending = null; commit(null); close(); };
popup.querySelector(".dp-today").onclick = () => {
pending = new Date(); pending.setSeconds(0, 0); view = new Date(); render();
};
popup.querySelector(".dp-done").onclick = () => { commit(pending); close(); };
}
function place() {
const r = input.getBoundingClientRect();
popup.style.top = `${r.bottom + 6}px`;
let left = r.left;
const pw = popup.offsetWidth;
if (left + pw > window.innerWidth - 8) left = Math.max(8, window.innerWidth - pw - 8);
popup.style.left = `${left}px`;
}
function open() {
if (isOpen()) return;
const utc = parseRaw(input.dataset.value);
pending = utc ? wallFromZoned(utcToZonedWall(utc)) : null;
view = pending ? new Date(pending) : new Date();
popup = document.createElement("div");
popup.className = "dp-pop";
document.body.appendChild(popup);
render();
place();
setTimeout(() => document.addEventListener("mousedown", onOutside), 0);
window.addEventListener("resize", place);
window.addEventListener("scroll", place, true);
document.addEventListener("keydown", onKey);
}
function close() {
if (!isOpen()) return;
document.removeEventListener("mousedown", onOutside);
window.removeEventListener("resize", place);
window.removeEventListener("scroll", place, true);
document.removeEventListener("keydown", onKey);
popup.remove();
popup = null;
}
function onOutside(e) {
if (popup && !popup.contains(e.target) && e.target !== input) close();
}
function onKey(e) {
if (e.key === "Escape") { close(); input.focus(); }
}
input.readOnly = true;
input.addEventListener("click", () => (isOpen() ? close() : open()));
input.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") { e.preventDefault(); open(); }
});
}