140 lines
3.9 KiB
Svelte
140 lines
3.9 KiB
Svelte
<script lang="ts">
|
|
import { Archive } from '@lucide/svelte';
|
|
|
|
type Props = {
|
|
onFilesSelected: (files: File[]) => void;
|
|
onFilesRejected?: (files: File[]) => void;
|
|
accept?: string;
|
|
multiple?: boolean;
|
|
ariaLabel?: string;
|
|
label?: string;
|
|
sublabel?: string;
|
|
icon?: typeof Archive;
|
|
class?: string;
|
|
};
|
|
|
|
let {
|
|
onFilesSelected,
|
|
onFilesRejected,
|
|
accept = '.pdf,application/pdf',
|
|
multiple = false,
|
|
ariaLabel = 'PDF-Dateien hinzufügen',
|
|
label = 'Dateien hinzufügen',
|
|
sublabel = 'PDF hier ablegen oder zum Auswählen klicken',
|
|
icon = undefined,
|
|
class: className = ''
|
|
}: Props = $props();
|
|
|
|
let fileInput = $state<HTMLInputElement | null>(null);
|
|
let isDragging = $state(false);
|
|
let rejectionMessage = $state<string | null>(null);
|
|
let dragDepth = 0;
|
|
|
|
const matchesAccept = (file: File) =>
|
|
accept
|
|
.split(',')
|
|
.map((value) => value.trim().toLowerCase())
|
|
.filter(Boolean)
|
|
.some((filter) => {
|
|
if (filter.startsWith('.')) return file.name.toLowerCase().endsWith(filter);
|
|
if (filter.endsWith('/*')) return file.type.toLowerCase().startsWith(filter.slice(0, -1));
|
|
return file.type.toLowerCase() === filter;
|
|
});
|
|
|
|
const processFiles = (files: FileList | null | undefined) => {
|
|
if (!files) return;
|
|
|
|
const received = Array.from(files);
|
|
const accepted = received.filter(matchesAccept);
|
|
const rejected = received.filter((file) => !matchesAccept(file));
|
|
|
|
if (rejected.length > 0) {
|
|
rejectionMessage = `${rejected.map((file) => file.name).join(', ')}: Dateityp nicht unterstützt.`;
|
|
onFilesRejected?.(rejected);
|
|
}
|
|
if (accepted.length > 0) {
|
|
rejectionMessage = null;
|
|
onFilesSelected(multiple ? accepted : accepted.slice(0, 1));
|
|
}
|
|
};
|
|
|
|
const openFileDialog = () => fileInput?.click();
|
|
|
|
const handleFileSelection = (event: Event) => {
|
|
const input = event.currentTarget as HTMLInputElement;
|
|
processFiles(input.files);
|
|
input.value = '';
|
|
};
|
|
|
|
const handleDragEnter = (event: DragEvent) => {
|
|
event.preventDefault();
|
|
dragDepth += 1;
|
|
isDragging = true;
|
|
};
|
|
|
|
const handleDragOver = (event: DragEvent) => {
|
|
event.preventDefault();
|
|
if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy';
|
|
};
|
|
|
|
const handleDragLeave = (event: DragEvent) => {
|
|
event.preventDefault();
|
|
dragDepth = Math.max(0, dragDepth - 1);
|
|
if (dragDepth === 0) isDragging = false;
|
|
};
|
|
|
|
const handleDrop = (event: DragEvent) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
dragDepth = 0;
|
|
isDragging = false;
|
|
processFiles(event.dataTransfer?.files);
|
|
};
|
|
</script>
|
|
|
|
<input
|
|
bind:this={fileInput}
|
|
class="hidden"
|
|
type="file"
|
|
{accept}
|
|
{multiple}
|
|
onchange={handleFileSelection}
|
|
/>
|
|
|
|
<button
|
|
type="button"
|
|
class="flex w-full cursor-pointer flex-col items-center justify-center gap-2 rounded-2xl border-2 border-dashed px-6 py-10 text-center transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-400 {isDragging
|
|
? 'scale-[1.02] border-primary bg-primary-100/70 shadow-lg ring-4 ring-primary/25'
|
|
: 'border-primary-300 bg-white hover:border-primary-400 hover:bg-primary-50/60'} {className}"
|
|
aria-label={ariaLabel}
|
|
data-dropzone-ready={fileInput ? 'true' : 'false'}
|
|
onclick={openFileDialog}
|
|
ondragenter={handleDragEnter}
|
|
ondragover={handleDragOver}
|
|
ondragleave={handleDragLeave}
|
|
ondrop={handleDrop}
|
|
>
|
|
{#if icon}
|
|
{@const IconComponent = icon}
|
|
<IconComponent
|
|
class="h-10 w-10 text-primary transition-transform duration-150 {isDragging
|
|
? 'scale-125'
|
|
: ''}"
|
|
/>
|
|
{/if}
|
|
<div class="flex flex-row items-baseline justify-center gap-2">
|
|
<span class="text-[18px] font-bold text-primary-900">{label}</span>
|
|
<span class="text-[20px] font-extrabold text-primary">+</span>
|
|
</div>
|
|
{#if sublabel}
|
|
<p class="text-[13px] text-primary-700">{sublabel}</p>
|
|
{/if}
|
|
{#if isDragging}
|
|
<p class="text-[13px] font-bold text-primary">Jetzt loslassen zum Hinzufügen</p>
|
|
{/if}
|
|
</button>
|
|
|
|
{#if rejectionMessage}
|
|
<p class="mt-2 text-sm font-medium text-red-700" role="alert">{rejectionMessage}</p>
|
|
{/if}
|