640 lines
14 KiB
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>
|