106 lines
3.4 KiB
JavaScript
106 lines
3.4 KiB
JavaScript
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 = typeof item?.confirm === 'function' ? item.confirm(result.value) :
|
|
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;
|