289 lines
11 KiB
TypeScript
289 lines
11 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
|
import { DateTime } from "luxon";
|
|
import { useFlash } from "../App/FlashContext";
|
|
import { useTimezone } from "../App/TimezoneProvider";
|
|
|
|
type TimeUpdate = {
|
|
year?: number | null;
|
|
month?: number;
|
|
day?: number;
|
|
hour?: number;
|
|
minute?: number;
|
|
second?: number;
|
|
};
|
|
|
|
export const TimeInput = ({ defaultValue, onInput }) => {
|
|
const ref = useRef(null);
|
|
|
|
const [timezone] = useTimezone();
|
|
const [time, setTime] = useState(
|
|
defaultValue ? DateTime.fromISO(defaultValue).setZone(timezone) : null,
|
|
);
|
|
|
|
const updateTime = useCallback(
|
|
(upd: TimeUpdate) => {
|
|
if (upd === null) {
|
|
setTime(null);
|
|
}
|
|
|
|
let newTime = time;
|
|
if (newTime === null) {
|
|
newTime = DateTime.now().setZone(timezone);
|
|
}
|
|
setTime(newTime.set(upd));
|
|
},
|
|
[time, timezone],
|
|
);
|
|
|
|
useEffect(() => {
|
|
onInput(time?.toFormat("yyyy-LL-dd'T'HH:mm:ss"));
|
|
}, [time]);
|
|
|
|
const flash = useFlash();
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
class={"input"}
|
|
onPaste={(ev) => {
|
|
ev.preventDefault();
|
|
const pasteValue = ev.clipboardData.getData("text/plain");
|
|
let dt = DateTime.fromISO(pasteValue);
|
|
|
|
if (dt.isValid) {
|
|
setTime(dt);
|
|
return;
|
|
}
|
|
|
|
dt = DateTime.fromSQL(pasteValue);
|
|
|
|
if (dt.isValid) {
|
|
setTime(dt);
|
|
return;
|
|
}
|
|
|
|
flash({
|
|
message: "Couldn't parse your clipboard data as a valid date-time",
|
|
type: "error",
|
|
});
|
|
}}
|
|
>
|
|
<div style={{ flexGrow: "1" }}>
|
|
<label>
|
|
<span class="is-sr-only">Years input</span>
|
|
<input
|
|
style={{
|
|
borderStyle: "none",
|
|
fontFamily: "monospace",
|
|
width: "calc(4ch + 4px)",
|
|
fontSize: "1rem",
|
|
}}
|
|
type="text"
|
|
pattern="\d*"
|
|
maxlength={4}
|
|
placeholder="YYYY"
|
|
value={
|
|
time
|
|
? time.year.toLocaleString("en-US", {
|
|
minimumIntegerDigits: 4,
|
|
useGrouping: false,
|
|
})
|
|
: ""
|
|
}
|
|
onBlur={(ev) => {
|
|
ev.currentTarget.value
|
|
? updateTime({
|
|
year: parseInt(ev.currentTarget.value),
|
|
})
|
|
: updateTime(null);
|
|
}}
|
|
></input>{" "}
|
|
</label>
|
|
-
|
|
<label>
|
|
<span class="is-sr-only">Months input</span>
|
|
<input
|
|
style={{
|
|
borderStyle: "none",
|
|
fontFamily: "monospace",
|
|
width: "calc(2ch + 4px)",
|
|
fontSize: "1rem",
|
|
}}
|
|
type="text"
|
|
pattern="\d*"
|
|
maxlength={2}
|
|
placeholder="MM"
|
|
value={
|
|
time
|
|
? time.month.toLocaleString("en-US", {
|
|
minimumIntegerDigits: 2,
|
|
})
|
|
: ""
|
|
}
|
|
onBlur={(ev) => {
|
|
ev.currentTarget.value
|
|
? updateTime({
|
|
month: parseInt(ev.currentTarget.value),
|
|
})
|
|
: updateTime(null);
|
|
}}
|
|
></input>{" "}
|
|
</label>
|
|
-
|
|
<label>
|
|
<span class="is-sr-only">Days input</span>
|
|
<input
|
|
style={{
|
|
borderStyle: "none",
|
|
fontFamily: "monospace",
|
|
width: "calc(2ch + 4px)",
|
|
fontSize: "1rem",
|
|
}}
|
|
type="text"
|
|
pattern="\d*"
|
|
maxlength={2}
|
|
placeholder="DD"
|
|
value={
|
|
time
|
|
? time.day.toLocaleString("en-US", { minimumIntegerDigits: 2 })
|
|
: ""
|
|
}
|
|
onBlur={(ev) => {
|
|
ev.currentTarget.value
|
|
? updateTime({
|
|
day: parseInt(ev.currentTarget.value),
|
|
})
|
|
: updateTime(null);
|
|
}}
|
|
></input>{" "}
|
|
</label>
|
|
<label style={{ marginLeft: "8px" }}>
|
|
<span class="is-sr-only">Hours input</span>
|
|
<input
|
|
style={{
|
|
borderStyle: "none",
|
|
fontFamily: "monospace",
|
|
width: "calc(2ch + 4px)",
|
|
fontSize: "1rem",
|
|
}}
|
|
type="text"
|
|
pattern="\d*"
|
|
maxlength={2}
|
|
placeholder="hh"
|
|
value={
|
|
time
|
|
? time.hour.toLocaleString("en-US", { minimumIntegerDigits: 2 })
|
|
: ""
|
|
}
|
|
onBlur={(ev) => {
|
|
ev.currentTarget.value
|
|
? updateTime({
|
|
hour: parseInt(ev.currentTarget.value),
|
|
})
|
|
: updateTime(null);
|
|
}}
|
|
></input>{" "}
|
|
</label>
|
|
:
|
|
<label>
|
|
<span class="is-sr-only">Minutes input</span>
|
|
<input
|
|
style={{
|
|
borderStyle: "none",
|
|
fontFamily: "monospace",
|
|
width: "calc(2ch + 4px)",
|
|
fontSize: "1rem",
|
|
}}
|
|
type="text"
|
|
pattern="\d*"
|
|
maxlength={2}
|
|
placeholder="mm"
|
|
value={
|
|
time
|
|
? time.minute.toLocaleString("en-US", {
|
|
minimumIntegerDigits: 2,
|
|
})
|
|
: ""
|
|
}
|
|
onBlur={(ev) => {
|
|
ev.currentTarget.value
|
|
? updateTime({
|
|
minute: parseInt(ev.currentTarget.value),
|
|
})
|
|
: updateTime(null);
|
|
}}
|
|
></input>{" "}
|
|
</label>
|
|
:
|
|
<label>
|
|
<span class="is-sr-only">Seconds input</span>
|
|
<input
|
|
style={{
|
|
borderStyle: "none",
|
|
fontFamily: "monospace",
|
|
width: "calc(2ch + 4px)",
|
|
fontSize: "1rem",
|
|
}}
|
|
type="text"
|
|
pattern="\d*"
|
|
maxlength={2}
|
|
placeholder="ss"
|
|
value={
|
|
time
|
|
? time.second.toLocaleString("en-US", {
|
|
minimumIntegerDigits: 2,
|
|
})
|
|
: ""
|
|
}
|
|
onBlur={(ev) => {
|
|
ev.currentTarget.value
|
|
? updateTime({
|
|
second: parseInt(ev.currentTarget.value),
|
|
})
|
|
: updateTime(null);
|
|
}}
|
|
></input>{" "}
|
|
</label>
|
|
</div>
|
|
<button
|
|
style={{
|
|
background: "none",
|
|
border: "none",
|
|
padding: "1px",
|
|
marginRight: "-3px",
|
|
}}
|
|
onClick={() => {
|
|
ref.current.showPicker();
|
|
}}
|
|
>
|
|
<span class="is-sr-only">Show time picker</span>
|
|
<span class="icon">
|
|
<i class="fas fa-calendar"></i>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
<input
|
|
style={{
|
|
position: "absolute",
|
|
left: 0,
|
|
visibility: "hidden",
|
|
}}
|
|
class={"input"}
|
|
type="datetime-local"
|
|
step="1"
|
|
value={
|
|
time
|
|
? time.toFormat("yyyy-LL-dd'T'HH:mm:ss")
|
|
: DateTime.now().toFormat("yyyy-LL-dd'T'HH:mm:ss")
|
|
}
|
|
ref={ref}
|
|
onInput={(ev) => {
|
|
ev.currentTarget.value === ""
|
|
? setTime(null)
|
|
: setTime(DateTime.fromISO(ev.currentTarget.value));
|
|
}}
|
|
></input>
|
|
</>
|
|
);
|
|
};
|