Stundenaufzeichnung/src/routes/record_input_row.svelte

640 lines
14 KiB
Svelte

<script module lang="ts">
export interface RowState {
date: { valid: boolean, value: string},
start: { valid: boolean, value: string},
end: { valid: boolean, value: string},
comment: { value: string }
}
</script>
<script lang="ts">
import type { Snippet } from "svelte";
import { toInt, parseDate, calculateDuration, weekday_of } from "$lib/util";
interface Props {
targetForm: string,
enabled: boolean,
autofocus: boolean,
states: RowState,
children: Snippet
}
let { targetForm, enabled, autofocus = false, states = $bindable(), children }: Props = $props();
const TODAY: Date = new Date();
const CENTURY_PREF: number = Math.floor(TODAY.getFullYear() / 100);
const CENTURY_YEAR: number = CENTURY_PREF * 100;
let dateInput: HTMLInputElement;
let startInput: HTMLInputElement;
let endInput: HTMLInputElement;
states = {
...states,
...(states?.date ? states.date : {}),
...(states?.start ? states.start : {}),
...(states?.end ? states.end : {}),
...(states?.comment ? states.comment : {}),
date: {
...states.date,
...(states.date?.valid == null && { valid: true }),
...(states.date?.value == null && { value: "" }),
},
start: {
...states.start,
...(states.start?.valid == null && { valid: true }),
...(states.start?.value == null && { value: "" }),
},
end: {
...states.end,
...(states.end?.valid == null && { valid: true }),
...(states.end?.value == null && { value: "" }),
},
comment: {
...states.comment,
...(states.comment?.value == null && { value: "" }),
},
};
let inWeekDay: string = $derived.by(() => {
let date = null;
if (states?.date?.valid && states?.date?.value != null) {
date = parseDate(states.date.value);
}
return weekday_of(date);
})
let inDuration: string = $state("");
$effect(() => {
states;
if ((states?.start?.valid && states?.end?.valid)
&& (states?.start?.value != null && states?.end?.value != null)) {
let duration = calculateDuration(states.start.value, states.end.value);
inDuration = isNaN(duration) ? "" : duration.toFixed(2);
}
})
function validateDate(element: HTMLInputElement) {
/*
todo: trailing . after month
supports:
D.M
DDMM
D.MM
DD.M
DD.MM
DDMMYY
D.M.YY
DD.M.YY
D.MM.YY
D.M.YYYY
DD.MM.YY
DDMMYYYY
DD.M.YYYY
D.MM.YYYY
DD.MM.YYYY
*/
switch (element.value.length) {
case 0: return true;
case 3: {
/*
* D.M
*/
let day = toInt(element.value.charAt(0));
let month = toInt(element.value.charAt(2));
if (isNaN(day) || isNaN(month) || element.value.charAt(1) !== '.') {
return false;
}
if (day > 0 && month > 0) {
element.value = "0" + day + ".0" + month + "." + TODAY.getFullYear();
return true;
}
} break;
case 4: if (
(() =>{
/*
* DDMM
*/
let day = toInt(element.value.slice(0, 2));
let month = toInt(element.value.slice(2, 4));
if (isNaN(day) || isNaN(month)) {
return false;
}
if (!(month < 1 || month > 12
|| day < 1 || day > new Date(TODAY.getFullYear(), month, 0).getDate())) {
element.value = element.value.slice(0, 2) + "." + element.value.slice(2, 4) + "." + TODAY.getFullYear();
return true;
}
return false;
})() === true
|| (() => {
/*
* DD.M
*/
let day = toInt(element.value.slice(0, 2));
let month = toInt(element.value.charAt(3));
if (isNaN(day) || isNaN(month) || element.value.charAt(2) !== '.') {
return false;
}
if (!(month < 1 || month > 12
|| day < 1 || day > new Date(TODAY.getFullYear(), month, 0).getDate())) {
element.value = element.value.slice(0, 2) + ".0" + month + "." + TODAY.getFullYear();
return true;
}
return false;
})() === true
|| (() => {
/*
* D.MM
*/
let day = toInt(element.value.charAt(0));
let month = toInt(element.value.slice(2, 4));
if (isNaN(day) || isNaN(month) || element.value.charAt(1) !== '.') {
return false;
}
if (!(month < 1 || month > 12
|| day < 1 || day > new Date(TODAY.getFullYear(), month, 0).getDate())) {
element.value = "0" + day + "." + element.value.slice(2, 4) + "." + TODAY.getFullYear();
return true;
}
return false;
})() === true) {
return true;
} break;
case 5: {
/*
* DD.MM
*/
let day = toInt(element.value.slice(0, 2));
let month = toInt(element.value.slice(3, 5));
if (isNaN(day) || isNaN(month) || element.value.charAt(2) !== '.') {
return false;
}
if (!((month < 1 || month > 12)
|| (day < 1 || day > new Date(TODAY.getFullYear(), month, 0).getDate()))) {
element.value = element.value + "." + TODAY.getFullYear();
return true;
}
} break;
case 6: if ((() => {
/*
* DDMMYY
*/
let day = toInt(element.value.slice(0, 2));
let month = toInt(element.value.slice(2, 4));
let year = toInt(element.value.slice(4, 6));
if (isNaN(day) || isNaN(month) || isNaN(year)) {
return false;
}
year += CENTURY_YEAR;
if (!(month < 1 || month > 12
|| day < 1 || day > new Date(year, month, 0).getDate())) {
element.value = element.value.slice(0, 2) + "." + element.value.slice(2, 4) + "." + year;
return true;
}
return false;
})() === true
|| (() => {
/*
* D.M.YY
*/
let day = toInt(element.value.charAt(0));
let month = toInt(element.value.charAt(2));
let year = toInt(element.value.slice(4, 6));
if (isNaN(day) || isNaN(month) || isNaN(year) || element.value.charAt(1) !== '.' || element.value.charAt(3) !== '.') {
return false;
}
year += CENTURY_YEAR;
if (!(month < 1 || month > 12
|| day < 1 || day > new Date(year, month, 0).getDate())) {
element.value = "0" + day + ".0" + month + "." + year;
return true;
}
return false;
})() === true) {
return true;
} break;
case 7: if ((() => {
/*
* D.MM.YY
*/
let day = toInt(element.value.charAt(0));
let month = toInt(element.value.slice(2, 4));
let year = toInt(element.value.slice(5, 7));
if (isNaN(day) || isNaN(month) || element.value.charAt(1) !== '.' || element.value.charAt(4) !== '.') {
return false;
}
year += CENTURY_YEAR;
if (!(month < 1 || month > 12
|| day < 1 || day > new Date(year, month, 0).getDate())) {
element.value = "0" + element.value.slice(0, 5) + year;
return true;
}
return false;
})() === true
|| (() => {
/*
* D.MM.YY
*/
let day = toInt(element.value.slice(0, 2));
let month = toInt(element.value.charAt(3));
let year = toInt(element.value.slice(5, 7));
if (isNaN(day) || isNaN(month) || element.value.charAt(2) !== '.' || element.value.charAt(4) !== '.') {
return false;
}
year += CENTURY_YEAR;
if (!(day < 1 || day > new Date(year, month, 0).getDate())) {
element.value = element.value.slice(0, 3) + "0" + month + "." + year;
return true;
}
return false;
})() === true) {
return true;
} break;
case 8: if (
(() => {
/*
* D.M.YYYY
*/
let day = toInt(element.value.charAt(0));
let month = toInt(element.value.charAt(2));
let year = toInt(element.value.slice(4, 8));
if (isNaN(day) || isNaN(month) || isNaN(year) || element.value.charAt(1) !== '.' || element.value.charAt(3) !== '.') {
return false;
}
year += CENTURY_YEAR;
if (!(month < 1 || month > 12
|| day < 1 || day > new Date(year, month, 0).getDate())) {
element.value = "0" + day + ".0" + month + "." + element.value.slice(4, 8);
return true;
}
return false;
})() === true
|| (() => {
/*
* DD.MM.YY
*/
let day = toInt(element.value.slice(0, 2));
let month = toInt(element.value.slice(3, 5));
let year = toInt(element.value.slice(6, 8));
if (isNaN(day) || isNaN(month) || isNaN(year) || element.value.charAt(2) !== '.' || element.value.charAt(5) !== '.') {
return false;
}
year += CENTURY_YEAR;
if (!(month < 1 || month > 12
|| day < 1 || day > new Date(year, month, 0).getDate())) {
element.value = element.value.slice(0, 2) + "." + element.value.slice(3, 5) + "." + CENTURY_PREF + element.value.slice(6, 8);
return true;
}
return false;
})() === true
|| (() => {
/*
* DDMMYYYY
*/
let day = toInt(element.value.slice(0, 2));
let month = toInt(element.value.slice(2, 4));
let year = toInt(element.value.slice(4, 8));
if (isNaN(day) || isNaN(month) || isNaN(year)) {
return false;
}
if (!(month < 1 || month > 12
|| day < 1 || day > new Date(year, month, 0).getDate())) {
element.value = element.value.slice(0, 2) + "." + element.value.slice(2, 4) + "." + element.value.slice(4, 8);
return true;
}
return false;
})() === true) {
return true;
} break;
case 9: if ((() => {
/*
* D.MM.YYYY
*/
let day = toInt(element.value.charAt(0));
let month = toInt(element.value.slice(2, 4));
let year = toInt(element.value.slice(5, 9));
if (isNaN(day) || isNaN(month) || element.value.charAt(1) !== '.' || element.value.charAt(4) !== '.') {
return false;
}
if (!(month < 1 || month > 12
|| day < 1 || day > new Date(year, month, 0).getDate())) {
element.value = "0" + element.value;
return true;
}
return false;
})() === true
|| (() => {
/*
* D.MM.YYYY
*/
let day = toInt(element.value.slice(0, 2));
let month = toInt(element.value.charAt(3));
let year = toInt(element.value.slice(5, 9));
if (isNaN(day) || isNaN(month) || element.value.charAt(2) !== '.' || element.value.charAt(4) !== '.') {
return false;
}
if (!(day < 1 || day > new Date(year, month, 0).getDate())) {
element.value = element.value.slice(0, 3) + "0" + month + element.value.slice(4, 9);
return true;
}
return false;
})() === true) {
return true;
} break;
case 10: {
/*
* DD.MM.YYYY
*/
let day = toInt(element.value.slice(0, 2));
let month = toInt(element.value.slice(3, 5));
let year = toInt(element.value.slice(6, 10));
if (isNaN(day) || isNaN(month) || isNaN(year) || element.value.charAt(2) !== '.' || element.value.charAt(5) !== '.') {
return false;
}
if (!(month < 1 || month > 12
|| day < 1 || day > new Date(year, month, 0).getDate())) {
return true;
}
} break;
}
return false;
}
function validateTime(element: HTMLInputElement): boolean {
/*
supports:
HH
H:MM
HHMM
HH:MM
*/
switch(element.value.length) {
case 0: return true;
case 2: {
let h = toInt(element.value);
if (!isNaN(h) && 0 < h && h <= 24) {
element.value = element.value + ":00";
return true;
}
} break;
case 4: if (
(() => {
let h = toInt(element.value.slice(0, 2));
let m = toInt(element.value.slice(2, 4));
if (isNaN(h) || isNaN(m)) {
return false;
}
if (0 <= h && h <= 24 && 0 <= m && m <= 59) {
element.value = element.value.slice(0, 2) + ":" + element.value.slice(2, 4);
return true;
}
return false;
})() === true
|| (() => {
let h = toInt(element.value.charAt(0));
let m = toInt(element.value.slice(2, 4));
if (isNaN(h) || isNaN(m) || element.value.charAt(1) !== ':') {
return false;
}
if (0 <= m && m <= 59) {
element.value = "0" + element.value;
return true;
}
return false;
})() === true) {
return true;
}
case 5: {
let h = toInt(element.value.slice(0, 2));
let m = toInt(element.value.slice(3, 5));
if (isNaN(h) || isNaN(m) || element.value.charAt(2) !== ':') {
return false;
}
if (0 <= h && h <= 24 && 0 <= m && m <= 59) {
return true;
}
} break;
}
return false;
}
</script>
<td>
<!-- svelte-ignore a11y_autofocus -->
<input
bind:this={dateInput}
bind:value={states.date.value}
class:form-invalid={!states.date.valid}
name="date"
type="text"
form={targetForm}
onfocusin={
(_) => {
dateInput.select();
states.date.valid = true;
}
}
onfocusout={
(_) => {
states.date.valid = validateDate(dateInput);
states.date.value = dateInput.value;
}
}
onkeydown={
(event) => {
if (event.key == "Enter") {
states.date.valid = validateDate(dateInput);
states.date.value = dateInput.value;
}
}
}
disabled={!enabled}
{autofocus}
required>
</td>
<td>
{inWeekDay}
</td>
<td>
<input
bind:this={startInput}
bind:value={states.start.value}
class:form-invalid={!states.start.valid}
name="start"
type="text"
form={targetForm}
onfocusin={
(_) => {
startInput.select();
states.start.valid = true;
}
}
onfocusout={
(_) => {
states.start.valid = validateTime(startInput);
states.start.value = startInput.value;
}
}
onkeydown={
(event) => {
if (event.key == "Enter") {
states.start.valid = validateTime(startInput);
states.start.value = startInput.value;
}
}
}
disabled={!enabled}
required>
</td>
<td>
<input
bind:this={endInput}
bind:value={states.end.value}
class:form-invalid={!states.end.valid}
name="end"
type="text"
form={targetForm}
onfocusin={
(_) => {
endInput.select();
states.end.valid = true;
}
}
onfocusout={
(_) => {
states.end.valid = validateTime(endInput);
states.end.value = endInput.value;
}
}
onkeydown={
(event) => {
if (event.key == "Enter") {
states.end.valid = validateTime(endInput);
states.end.value = endInput.value;
}
}
}
disabled={!enabled}
required>
</td>
<td>
{inDuration}
</td>
<td>
<input
name="comment"
type="text"
form={targetForm}
value={states.comment.value}
disabled={!enabled}>
</td>
<td class="action">
{@render children?.()}
</td>
<style>
td input {
box-sizing: border-box;
width: 100%;
vertical-align: middle;
border: 1px solid gray;
}
.action {
display: inline-flex;
}
.form-invalid {
background: #FF4444;
}
</style>