258 lines
7.9 KiB
Svelte
258 lines
7.9 KiB
Svelte
<script lang="ts">
|
|
import Input from '@components/input/Input.svelte';
|
|
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
|
|
import Textarea from '@components/input/Textarea.svelte';
|
|
import Password from '@components/input/Password.svelte';
|
|
import BitBadge from '@components/input/BitBadge.svelte';
|
|
import UserSearch from '@components/admin/search/UserSearch.svelte';
|
|
import Checkbox from '@components/input/Checkbox.svelte';
|
|
import TeamSearch from '@components/admin/search/TeamSearch.svelte';
|
|
|
|
// html bindings
|
|
let modal: HTMLDialogElement;
|
|
let modalForm: HTMLFormElement;
|
|
|
|
// types
|
|
interface Props<T> {
|
|
texts: {
|
|
title: string;
|
|
submitButtonTitle: string;
|
|
confirmPopupTitle: string;
|
|
confirmPopupMessage: string;
|
|
};
|
|
|
|
target: T | null;
|
|
keys: Key<any>[][];
|
|
|
|
onSubmit: (target: T) => void;
|
|
onClose?: () => void;
|
|
|
|
open: boolean;
|
|
}
|
|
type Key<T extends KeyInputType> = {
|
|
key: string;
|
|
type: T | null;
|
|
label: string;
|
|
default?: any;
|
|
options?: KeyInputTypeOptions<T>;
|
|
};
|
|
type KeyInputType =
|
|
| 'bit-badge'
|
|
| 'checkbox'
|
|
| 'color'
|
|
| 'date'
|
|
| 'datetime-local'
|
|
| 'number'
|
|
| 'password'
|
|
| 'tel'
|
|
| 'team-search'
|
|
| 'text'
|
|
| 'textarea'
|
|
| 'user-search';
|
|
type KeyInputTypeOptions<T extends KeyInputType> = {
|
|
['bit-badge']: {
|
|
available: Record<number, string>;
|
|
};
|
|
['checkbox']: {};
|
|
['color']: {};
|
|
['date']: {};
|
|
['datetime-local']: {};
|
|
['number']: {};
|
|
['password']: {};
|
|
['tel']: {};
|
|
['team-search']: {};
|
|
['text']: {};
|
|
['textarea']: {
|
|
rows?: boolean;
|
|
};
|
|
['user-search']: {
|
|
mustMatch?: boolean;
|
|
};
|
|
}[T] & {
|
|
convert?: (value: any) => any;
|
|
validate?: (value: any) => boolean;
|
|
required?: boolean;
|
|
dynamicWidth?: boolean;
|
|
};
|
|
|
|
// input
|
|
let { texts, target, keys, onSubmit, onClose, open = $bindable() }: Props<any> = $props();
|
|
|
|
onInit();
|
|
|
|
// state
|
|
let submitEnabled = $state(false);
|
|
|
|
$effect(() => {
|
|
if (!open) return;
|
|
|
|
onInit();
|
|
modal.show();
|
|
});
|
|
|
|
$effect.pre(() => {
|
|
if (target == null) onInit();
|
|
updateSubmitEnabled();
|
|
});
|
|
|
|
// function
|
|
function onInit() {
|
|
if (target == null) target = {};
|
|
|
|
for (const key of keys) {
|
|
for (const k of key) {
|
|
if (k.default !== undefined && target[k.key] === undefined)
|
|
target[k.key] = k.options?.convert ? k.options.convert(k.default) : k.default;
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateSubmitEnabled() {
|
|
submitEnabled = false;
|
|
for (const key of keys) {
|
|
for (const k of key) {
|
|
if (k.options?.required) {
|
|
if (k.options?.validate) {
|
|
if (!k.options.validate(target[k.key])) return;
|
|
} else {
|
|
if (!target[k.key]) return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
submitEnabled = true;
|
|
}
|
|
|
|
// callbacks
|
|
function onBindChange(key: string, value: any, options?: KeyInputTypeOptions<any>) {
|
|
target[key] = options?.convert ? options.convert(value) : value;
|
|
|
|
updateSubmitEnabled();
|
|
}
|
|
|
|
async function onSaveButtonClick(e: Event) {
|
|
e.preventDefault();
|
|
$confirmPopupState = {
|
|
title: texts.confirmPopupTitle,
|
|
message: texts.confirmPopupMessage,
|
|
onConfirm: () => {
|
|
modalForm.submit();
|
|
onSubmit(target);
|
|
}
|
|
};
|
|
}
|
|
|
|
function onCancelButtonClick(e: Event) {
|
|
e.preventDefault();
|
|
modalForm.submit();
|
|
}
|
|
|
|
function onModalClose() {
|
|
setTimeout(() => {
|
|
open = false;
|
|
target = null;
|
|
modalForm.reset();
|
|
onClose?.();
|
|
}, 300);
|
|
}
|
|
</script>
|
|
|
|
<dialog class="modal" bind:this={modal} onclose={onModalClose}>
|
|
<form method="dialog" class="modal-box overflow-visible" bind:this={modalForm}>
|
|
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick={onCancelButtonClick}>✕</button>
|
|
<div class="space-y-5">
|
|
<h3 class="text-xl font-geist font-bold">{texts.title}</h3>
|
|
<div class="w-full flex flex-col">
|
|
{#each keys as key (key)}
|
|
<div
|
|
class="grid grid-flow-col gap-4"
|
|
class:grid-cols-1={key.length === 1}
|
|
class:grid-cols-2={key.length === 2}
|
|
>
|
|
{#each key as k (k)}
|
|
{#if k.type === 'color' || k.type === 'date' || k.type === 'datetime-local' || k.type === 'number' || k.type === 'tel' || k.type === 'text'}
|
|
<Input
|
|
type={k.type}
|
|
bind:value={() => target[k.key] ?? k.default, (v) => onBindChange(k.key, v, k.options)}
|
|
label={k.label}
|
|
required={k.options?.required}
|
|
dynamicWidth={k.options?.dynamicWidth}
|
|
/>
|
|
{:else if k.type === 'textarea'}
|
|
<Textarea
|
|
bind:value={() => target[k.key] ?? k.default, (v) => onBindChange(k.key, v, k.options)}
|
|
label={k.label}
|
|
required={k.options?.required}
|
|
rows={k.options?.rows}
|
|
dynamicWidth={k.options?.dynamicWidth}
|
|
/>
|
|
{:else if k.type === 'checkbox'}
|
|
<Checkbox
|
|
bind:checked={() => target[k.key] ?? k.default, (v) => onBindChange(k.key, v, k.options)}
|
|
label={k.label}
|
|
required={k.options?.required}
|
|
/>
|
|
{:else if k.type === 'user-search'}
|
|
<UserSearch
|
|
bind:value={
|
|
() =>
|
|
target[k.key] ? (target[k.key]['username'] ?? null) : k.default ? k.default['username'] : null,
|
|
(v) =>
|
|
k.options.mustMatch
|
|
? onBindChange(k.key, { id: null, username: null }, k.options)
|
|
: onBindChange(k.key, { id: null, username: v }, k.options)
|
|
}
|
|
onSubmit={(user) => onBindChange(k.key, user, k.options)}
|
|
label={k.label}
|
|
required={k.options?.required}
|
|
mustMatch={k.options?.mustMatch}
|
|
/>
|
|
{:else if k.type === 'team-search'}
|
|
<TeamSearch
|
|
bind:value={
|
|
() => (target[k.key] ? (target[k.key]['name'] ?? null) : k.default ? k.default['name'] : null),
|
|
(v) =>
|
|
k.options.mustMatch
|
|
? onBindChange(k.key, { id: null, name: null, color: null }, k.options)
|
|
: onBindChange(k.key, { id: null, name: v, color: null }, k.options)
|
|
}
|
|
onSubmit={(team) => onBindChange(k.key, team, k.options)}
|
|
label={k.label}
|
|
required={k.options?.required}
|
|
mustMatch={k.options?.mustMatch}
|
|
/>
|
|
{:else if k.type === 'password'}
|
|
<Password
|
|
bind:value={() => target[k.key] ?? k.default, (v) => onBindChange(k.key, v, k.options)}
|
|
label={k.label}
|
|
required={k.options?.required}
|
|
/>
|
|
{:else if k.type === 'bit-badge'}
|
|
<BitBadge
|
|
available={k.options?.available}
|
|
bind:value={
|
|
() => (target[k.key] ? (target[k.key]['name'] ?? target[k.key]) : k.default),
|
|
(v) => onBindChange(k.key, v, k.options)
|
|
}
|
|
/>
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
<div>
|
|
<button
|
|
class="btn btn-success"
|
|
class:disabled={!submitEnabled}
|
|
disabled={!submitEnabled}
|
|
onclick={onSaveButtonClick}>{texts.submitButtonTitle}</button
|
|
>
|
|
<button class="btn btn-error" type="button" onclick={onCancelButtonClick}>Abbrechen</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
<form method="dialog" class="modal-backdrop bg-[rgba(0,0,0,.3)]">
|
|
<button class="!cursor-default">close</button>
|
|
</form>
|
|
</dialog>
|