function createLaterPicker({ now = () => new Date(), history = null, eventTarget = null, onState = () => {}, onConfirm = () => false, } = {}) { let active = false; let item = null; let trigger = null; let context = null; let afterClose = null; let committed = false; let started = false; function parse(input) { const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/.exec(String(input || '')); if (!match) return { ok:false, message:'Choose a valid local date and time.' }; const parts = match.slice(1).map(Number); const value = new Date(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], 0, 0); const sameLocalTime = value.getFullYear() === parts[0] && value.getMonth() === parts[1] - 1 && value.getDate() === parts[2] && value.getHours() === parts[3] && value.getMinutes() === parts[4]; if (!sameLocalTime) return { ok:false, message:'Choose a valid local date and time.' }; if (value <= now()) return { ok:false, message:'Choose a future date and time.' }; return { ok:true, value }; } function formatLocal(value) { const pad = part => String(part).padStart(2, '0'); return value.getFullYear() + '-' + pad(value.getMonth() + 1) + '-' + pad(value.getDate()) + 'T' + pad(value.getHours()) + ':' + pad(value.getMinutes()); } function deactivate() { if (!active) return false; const restore = trigger; active = false; item = null; trigger = null; context = null; committed = false; onState({ open:false, message:'' }); restore?.focus?.(); const cleanup = afterClose; afterClose = null; cleanup?.(); return true; } function start() { if (started || !eventTarget) return; started = true; eventTarget.addEventListener('popstate', event => { if (active && !event.state?.laterPicker) deactivate(); }); eventTarget.addEventListener('keydown', event => { if (active && event.key === 'Escape') { event.preventDefault?.(); close(); } }); } function open(nextItem, nextTrigger, nextContext = 'card') { if (!nextItem || active) return false; active = true; item = nextItem; trigger = nextTrigger || null; context = nextContext; committed = false; const suggested = new Date(now().getTime() + 60 * 60 * 1000); suggested.setMinutes(Math.ceil(suggested.getMinutes() / 15) * 15, 0, 0); if (history) history.pushState({ ...(history.state || {}), laterPicker:true }, ''); onState({ open:true, message:'', value:formatLocal(suggested) }); return true; } function close() { if (!active) return false; if (history?.state?.laterPicker) history.back(); else deactivate(); return true; } function submit(input) { if (!active || committed) return false; const result = parse(input); if (!result.ok) { onState({ open:true, message:result.message, value:String(input || '') }); return false; } const confirmed = onConfirm(item, result.value, context); if (confirmed !== true && typeof confirmed !== 'function') return false; committed = true; afterClose = typeof confirmed === 'function' ? confirmed : null; close(); return true; } return { parse, start, open, close, submit, current:() => active }; } if (typeof module !== 'undefined' && module.exports) module.exports = createLaterPicker;