This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
<script lang="ts">
|
||||
type Props = {
|
||||
onFilesSelected: (files: File[]) => void;
|
||||
onFilesRejected?: (files: File[]) => void;
|
||||
accept?: string;
|
||||
multiple?: boolean;
|
||||
ariaLabel?: string;
|
||||
label?: string;
|
||||
sublabel?: string;
|
||||
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',
|
||||
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 border-primary-200 bg-primary-50/50 px-6 py-10 text-center transition focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-400 {isDragging
|
||||
? 'border-primary-400 bg-primary-100/60'
|
||||
: ''} {className}"
|
||||
aria-label={ariaLabel}
|
||||
data-dropzone-ready={fileInput ? 'true' : 'false'}
|
||||
onclick={openFileDialog}
|
||||
ondragenter={handleDragEnter}
|
||||
ondragover={handleDragOver}
|
||||
ondragleave={handleDragLeave}
|
||||
ondrop={handleDrop}
|
||||
>
|
||||
<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}
|
||||
</button>
|
||||
|
||||
{#if rejectionMessage}
|
||||
<p class="mt-2 text-sm font-medium text-red-700" role="alert">{rejectionMessage}</p>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user