141 lines
4.1 KiB
Svelte
141 lines
4.1 KiB
Svelte
<script lang="ts">
|
|
import { onDestroy } from 'svelte';
|
|
|
|
let {
|
|
name,
|
|
accept,
|
|
required = false,
|
|
currentUrl = '',
|
|
placeholderUrl = '',
|
|
emptyLabel = 'NO IMAGE',
|
|
browseLabel = 'CHOOSE FILE',
|
|
resetToken = 0
|
|
}: {
|
|
name: string;
|
|
accept: string;
|
|
required?: boolean;
|
|
currentUrl?: string | null;
|
|
placeholderUrl?: string | null;
|
|
emptyLabel?: string;
|
|
browseLabel?: string;
|
|
resetToken?: string | number;
|
|
} = $props();
|
|
|
|
let input: HTMLInputElement | undefined;
|
|
let previewUrl = $state('');
|
|
let fileName = $state('');
|
|
let dragging = $state(false);
|
|
let error = $state('');
|
|
let handledResetToken: string | number | null = null;
|
|
|
|
function resetUpload() {
|
|
if (previewUrl) URL.revokeObjectURL(previewUrl);
|
|
previewUrl = '';
|
|
fileName = '';
|
|
dragging = false;
|
|
error = '';
|
|
if (input) input.value = '';
|
|
}
|
|
|
|
$effect(() => {
|
|
if (handledResetToken === null) {
|
|
handledResetToken = resetToken;
|
|
return;
|
|
}
|
|
if (resetToken !== handledResetToken) {
|
|
handledResetToken = resetToken;
|
|
resetUpload();
|
|
}
|
|
});
|
|
|
|
function setFile(file: File | undefined) {
|
|
if (!file || !input) return;
|
|
if (!file.type.startsWith('image/')) {
|
|
error = 'Clipboard or dropped content must be an image.';
|
|
return;
|
|
}
|
|
const transfer = new DataTransfer();
|
|
transfer.items.add(file);
|
|
input.files = transfer.files;
|
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
|
|
function choose(event: Event) {
|
|
const file = (event.currentTarget as HTMLInputElement).files?.[0];
|
|
if (previewUrl) URL.revokeObjectURL(previewUrl);
|
|
previewUrl = file ? URL.createObjectURL(file) : '';
|
|
fileName = file?.name ?? '';
|
|
error = '';
|
|
}
|
|
|
|
function drop(event: DragEvent) {
|
|
event.preventDefault();
|
|
dragging = false;
|
|
setFile(Array.from(event.dataTransfer?.files ?? []).find((file) => file.type.startsWith('image/')));
|
|
}
|
|
|
|
function paste(event: ClipboardEvent) {
|
|
const file = Array.from(event.clipboardData?.files ?? []).find((item) => item.type.startsWith('image/'));
|
|
if (file) {
|
|
event.preventDefault();
|
|
setFile(file);
|
|
} else {
|
|
error = 'No image was found on the clipboard.';
|
|
}
|
|
}
|
|
|
|
async function pasteFromClipboard() {
|
|
error = '';
|
|
try {
|
|
const items = await navigator.clipboard.read();
|
|
for (const item of items) {
|
|
const type = item.types.find((candidate) => candidate.startsWith('image/'));
|
|
if (type) {
|
|
const blob = await item.getType(type);
|
|
setFile(new File([blob], `pasted-image.${type.split('/')[1] || 'png'}`, { type }));
|
|
return;
|
|
}
|
|
}
|
|
error = 'No image was found on the clipboard.';
|
|
} catch {
|
|
error = 'Clipboard access was blocked. Focus the drop zone and press Ctrl+V instead.';
|
|
}
|
|
}
|
|
|
|
onDestroy(() => {
|
|
if (previewUrl) URL.revokeObjectURL(previewUrl);
|
|
});
|
|
</script>
|
|
|
|
<div
|
|
class="image-drop-input"
|
|
class:dragging
|
|
role="group"
|
|
aria-label="Drop an image here or paste one from the clipboard"
|
|
ondragenter={(event) => { event.preventDefault(); dragging = true; }}
|
|
ondragover={(event) => event.preventDefault()}
|
|
ondragleave={(event) => {
|
|
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) dragging = false;
|
|
}}
|
|
ondrop={drop}
|
|
onpaste={paste}
|
|
>
|
|
<input bind:this={input} class="image-file-input" {name} type="file" {accept} {required} onchange={choose}/>
|
|
<div class="image-upload-preview">
|
|
{#if previewUrl || currentUrl || placeholderUrl}
|
|
<img class:missing-item-icon={!previewUrl && !currentUrl && Boolean(placeholderUrl)} src={previewUrl || currentUrl || placeholderUrl || ''} alt={previewUrl || currentUrl ? 'Selected upload preview' : emptyLabel}/>
|
|
{:else}
|
|
<span>{emptyLabel}</span>
|
|
{/if}
|
|
</div>
|
|
<div class="image-drop-copy">
|
|
<b>{dragging ? 'RELEASE TO SELECT' : 'DROP IMAGE HERE'}</b>
|
|
<span>{fileName || 'OR USE AN UPLOAD METHOD'}</span>
|
|
</div>
|
|
<div class="image-upload-actions">
|
|
<button type="button" class="secondary" onclick={(event) => { event.stopPropagation(); input?.click(); }}>{browseLabel}</button>
|
|
<button type="button" class="secondary" onclick={(event) => { event.stopPropagation(); pasteFromClipboard(); }}>PASTE IMAGE</button>
|
|
</div>
|
|
</div>
|
|
{#if error}<small class="image-upload-error" role="alert">{error}</small>{/if}
|