177 lines
4.7 KiB
Svelte
177 lines
4.7 KiB
Svelte
<!--
|
|
Installed from @ieedan/shadcn-svelte-extras
|
|
-->
|
|
|
|
<script lang="ts">
|
|
import { cn } from "$lib/utils";
|
|
import UploadIcon from "@lucide/svelte/icons/upload";
|
|
import { displaySize } from ".";
|
|
import { useId } from "bits-ui";
|
|
import type { FileDropZoneProps, FileRejectedReason } from "./types";
|
|
|
|
let {
|
|
id = useId(),
|
|
children,
|
|
maxFiles,
|
|
maxFileSize,
|
|
fileCount,
|
|
disabled = false,
|
|
onUpload,
|
|
onFileRejected,
|
|
accept,
|
|
class: className,
|
|
...rest
|
|
}: FileDropZoneProps = $props();
|
|
|
|
if (maxFiles !== undefined && fileCount === undefined) {
|
|
console.warn(
|
|
"Make sure to provide FileDropZone with `fileCount` when using the `maxFiles` prompt",
|
|
);
|
|
}
|
|
|
|
let uploading = $state(false);
|
|
|
|
const drop = async (
|
|
e: DragEvent & {
|
|
currentTarget: EventTarget & HTMLLabelElement;
|
|
},
|
|
) => {
|
|
if (disabled || !canUploadFiles) return;
|
|
|
|
e.preventDefault();
|
|
|
|
const droppedFiles = Array.from(e.dataTransfer?.files ?? []);
|
|
|
|
await upload(droppedFiles);
|
|
};
|
|
|
|
const change = async (
|
|
e: Event & {
|
|
currentTarget: EventTarget & HTMLInputElement;
|
|
},
|
|
) => {
|
|
if (disabled) return;
|
|
|
|
const selectedFiles = e.currentTarget.files;
|
|
|
|
if (!selectedFiles) return;
|
|
|
|
await upload(Array.from(selectedFiles));
|
|
|
|
// this if a file fails and we upload the same file again we still get feedback
|
|
(e.target as HTMLInputElement).value = "";
|
|
};
|
|
|
|
const shouldAcceptFile = (file: File, fileNumber: number): FileRejectedReason | undefined => {
|
|
if (maxFileSize !== undefined && file.size > maxFileSize) return "Maximum file size exceeded";
|
|
|
|
if (maxFiles !== undefined && fileNumber > maxFiles) return "Maximum files uploaded";
|
|
|
|
if (!accept) return undefined;
|
|
|
|
const acceptedTypes = accept.split(",").map((a) => a.trim().toLowerCase());
|
|
const fileType = file.type.toLowerCase();
|
|
const fileName = file.name.toLowerCase();
|
|
|
|
const isAcceptable = acceptedTypes.some((pattern) => {
|
|
// check extension like .mp4
|
|
if (fileType.startsWith(".")) {
|
|
return fileName.endsWith(pattern);
|
|
}
|
|
|
|
// if pattern has wild card like video/*
|
|
if (pattern.endsWith("/*")) {
|
|
const baseType = pattern.slice(0, pattern.indexOf("/*"));
|
|
return fileType.startsWith(baseType + "/");
|
|
}
|
|
|
|
// otherwise it must be a specific type like video/mp4
|
|
return fileType === pattern;
|
|
});
|
|
|
|
if (!isAcceptable) return "File type not allowed";
|
|
|
|
return undefined;
|
|
};
|
|
|
|
const upload = async (uploadFiles: File[]) => {
|
|
uploading = true;
|
|
|
|
const validFiles: File[] = [];
|
|
|
|
for (let i = 0; i < uploadFiles.length; i++) {
|
|
const file = uploadFiles[i];
|
|
|
|
const rejectedReason = shouldAcceptFile(file, (fileCount ?? 0) + i + 1);
|
|
|
|
if (rejectedReason) {
|
|
onFileRejected?.({ file, reason: rejectedReason });
|
|
continue;
|
|
}
|
|
|
|
validFiles.push(file);
|
|
}
|
|
|
|
await onUpload(validFiles);
|
|
|
|
uploading = false;
|
|
};
|
|
|
|
const canUploadFiles = $derived(
|
|
!disabled &&
|
|
!uploading &&
|
|
!(maxFiles !== undefined && fileCount !== undefined && fileCount >= maxFiles),
|
|
);
|
|
</script>
|
|
|
|
<label
|
|
ondragover={(e) => e.preventDefault()}
|
|
ondrop={drop}
|
|
for={id}
|
|
aria-disabled={!canUploadFiles}
|
|
class={cn(
|
|
"border-border hover:bg-accent/25 flex h-48 w-full place-items-center justify-center rounded-lg border-2 border-dashed p-6 transition-all hover:cursor-pointer aria-disabled:opacity-50 aria-disabled:hover:cursor-not-allowed",
|
|
className,
|
|
)}
|
|
>
|
|
{#if children}
|
|
{@render children()}
|
|
{:else}
|
|
<div class="flex flex-col place-items-center justify-center gap-2">
|
|
<div
|
|
class="border-border text-muted-foreground flex size-14 place-items-center justify-center rounded-full border border-dashed"
|
|
>
|
|
<UploadIcon class="size-7" />
|
|
</div>
|
|
<div class="flex flex-col gap-0.5 text-center">
|
|
<span class="text-muted-foreground font-medium">
|
|
Drag 'n' drop files here, or click to select files
|
|
</span>
|
|
{#if maxFiles || maxFileSize}
|
|
<span class="text-muted-foreground/75 text-sm">
|
|
{#if maxFiles}
|
|
<span>You can upload {maxFiles} files</span>
|
|
{/if}
|
|
{#if maxFiles && maxFileSize}
|
|
<span>(up to {displaySize(maxFileSize)} each)</span>
|
|
{/if}
|
|
{#if maxFileSize && !maxFiles}
|
|
<span>Maximum size {displaySize(maxFileSize)}</span>
|
|
{/if}
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
<input
|
|
{...rest}
|
|
disabled={!canUploadFiles}
|
|
{id}
|
|
{accept}
|
|
multiple={maxFiles === undefined || maxFiles - (fileCount ?? 0) > 1}
|
|
type="file"
|
|
onchange={change}
|
|
class="hidden"
|
|
/>
|
|
</label>
|