file_drop
file_drop expands directories, applies file-input MIME/extension filtering and reports processing errors. Drops without accepted files are ignored. on_files receives an AbortSignal; each newer accepted drop or cleanup/recreation cooperatively aborts the previous callback.
No files selected
svelte<script lang="ts">
import { file_drop } from '$lib/attachments'
import { filter_accepted_files } from '$lib/file-drop'
let names = $state<string[]>([])
let drag_active = $state(false)
const accepted_file_types = `image/*,.pdf`
const allow_multiple = true
const show_files = (files: File[]) => (names = files.map(({ name }) => name))
</script>
<label
data-active={drag_active}
style="display: block; padding: 1rem; border: 1px dashed currentColor"
{@attach file_drop({
accept: accepted_file_types,
multiple: allow_multiple,
on_drag_active: (active) => (drag_active = active),
on_files: async (files, signal) => {
// `signal` only aborts on a newer drop, so the picker below would lose a race
// with this await. Every writer replaces `names`, so its identity settles it.
const reported = names
await Promise.all(files.map((file) => file.arrayBuffer()))
if (!signal.aborted && names === reported) show_files(files)
},
on_error: (error) => (names = [`Error: ${String(error)}`]),
})}
>
{drag_active
? `Release files`
: `Drop images or PDFs (${accepted_file_types}; multiple: ${allow_multiple})`}
<input
type="file"
accept={accepted_file_types}
multiple={allow_multiple}
onchange={(event) =>
show_files(
filter_accepted_files(
event.currentTarget.files ?? [],
accepted_file_types,
allow_multiple,
),
)}
/>
</label>
<p aria-live="polite">{names.join(`, `) || `No files selected`}</p>