<!-- Generated by tools/docs.php from docs/src/upload.html - edit the source, not this file. -->

# File upload

Photos, documents, a price list: chosen, dropped or pasted, each file checked at once and sent on its own, with its own progress and its own error.

## Examples

`upload` holds the file input, an `upload-zone` button that opens it and takes files dropped on it, a `<template>` of one file's row and an `upload-list`. A paste while the focus is in the upload takes the clipboard's files. Each file is checked against the input's `accept` and the upload's `data-max-size` in bytes; a refusal shows in the file's row, the page's text `file.type` or `file.size`. A file dropped beside the zone does not open in the window.

```html
<div class="upload" data-max-size="2097152" data-docs-upload>
	<input class="visually-hidden" id="upload-photos-input" type="file" name="photos[]" accept="image/*,.pdf" multiple tabindex="-1" aria-label="Photos">
	<button class="upload-zone" type="button" aria-describedby="upload-photos-hint">
		<i class="bi bi-cloud-arrow-up fs-24" aria-hidden="true"></i>
		<span>Drop photos here, or choose them</span>
		<span class="fs-12" id="upload-photos-hint">JPEG, PNG or PDF, up to 2 MB each</span>
	</button>
	<ul class="upload-list" aria-label="Files"></ul>
	<template>
		<li>
			<i class="bi bi-file-earmark-image" aria-hidden="true"></i>
			<span class="flex-fill" data-file-name></span>
			<progress class="progress" max="1" value="0" aria-label="Sent"></progress>
			<span class="invalid-feedback" data-file-error></span>
			<button class="btn btn-sm btn-icon btn-text" type="button" data-file-remove><i class="bi bi-x-lg" aria-hidden="true"></i></button>
		</li>
	</template>
</div>
```

### One file

An input without `multiple` holds one file: a new one chosen, dropped or pasted takes the place of the one before - its row, its sending, its place in the input - and of several dropped at once only the first is taken.

```html
<div class="upload" data-max-size="2097152">
	<input class="visually-hidden" id="upload-one-input" type="file" name="photo" accept="image/*" tabindex="-1" aria-label="Reference photo">
	<button class="upload-zone" type="button" aria-describedby="upload-one-hint">
		<i class="bi bi-image fs-24" aria-hidden="true"></i>
		<span>Drop a photo here, or choose one</span>
		<span class="fs-12" id="upload-one-hint">One image, up to 2 MB</span>
	</button>
	<ul class="upload-list" aria-label="File"></ul>
	<template>
		<li>
			<i class="bi bi-file-earmark-image" aria-hidden="true"></i>
			<span class="flex-fill" data-file-name></span>
			<span class="invalid-feedback" data-file-error></span>
			<button class="btn btn-sm btn-icon btn-text" type="button" data-file-remove><i class="bi bi-x-lg" aria-hidden="true"></i></button>
		</li>
	</template>
</div>
```

### Thumbnails

`upload-thumbs` on the upload draws its files as 5rem squares: an `img data-file-preview` in the row's template shows the image itself, cut to fill, its remove button on the corner; a refused file keeps a whole row for its words. `data-max-files` takes that many and refuses the rest with the page's text `file.count`. A page that prepares each image first - resized, turned upright - answers `redz:files` with a promise of the new file, which takes the old one's place in the input.

```html
<div class="upload upload-thumbs" data-max-files="3" data-max-size="5242880">
	<input class="visually-hidden" id="upload-shots-input" type="file" name="shots[]" accept="image/*" multiple tabindex="-1" aria-label="Screenshots">
	<button class="upload-zone" type="button" aria-describedby="upload-shots-hint">
		<i class="bi bi-images fs-24" aria-hidden="true"></i>
		<span>Drop or paste screenshots here, or choose them</span>
		<span class="fs-12" id="upload-shots-hint">Up to 3 images</span>
	</button>
	<ul class="upload-list" aria-label="Screenshots"></ul>
	<template>
		<li>
			<img data-file-preview alt="">
			<span data-file-name></span>
			<span class="invalid-feedback" data-file-error></span>
			<button class="btn btn-xs btn-icon" type="button" data-file-remove><i class="bi bi-x-lg" aria-hidden="true"></i></button>
		</li>
	</template>
</div>
```

```js
upload.addEventListener('redz:files', (event) => {
  event.preventDefault();
  event.detail.respondWith(createImageBitmap(event.detail.file, {imageOrientation: 'from-image'}).then(resize));
});
```

### Sent by the page

Each file fires `redz:files` on the upload with the file, its row, `progress(fraction)` for the row's bar and a `signal` its remove button aborts. A page that sends cancels it and answers with a promise: resolved, the row is done; resolved with the id the server gave the file - a string or a number - the row is a stored one from then on, as the server would write it, `data-file-stored` its id, and its Remove asks the page in `redz:remove`; rejected, the page's words show in the row's `data-file-error`. One file's error leaves the others. Left alone, the file joins the input's files and goes with the form. The example above answers in a second and a half.

```js
document.addEventListener('redz:files', (event) => {
	event.preventDefault();
	const {file, progress, signal} = event.detail;
	event.detail.respondWith(new Promise((sent, failed) => {
		const request = new XMLHttpRequest();
		request.open('POST', '/files');
		request.upload.onprogress = (step) => progress(step.loaded / step.total);
		request.onload = () => (request.status === 200 ? sent(request.responseText) : failed(request.responseText));
		request.onerror = () => failed('The file did not arrive');
		signal.addEventListener('abort', () => request.abort());
		request.send(file);
	}));
});
```

### Files the server holds

The server writes a row for each file it holds, as the template's, with `data-file-stored` - its id - and the name of its remove button. Remove asks the page in `redz:remove` with `detail.id` and `detail.row`: a page that cancels it and answers with a promise keeps the row busy until it settles - gone when it resolves, its words said in its `data-file-error` when it rejects. Left alone, the row goes, and a hidden field of its own with it, so the form sends the ids still kept. After each change the upload fires `redz:change` with `detail.count`, the files it holds, stored and new; `data-max-files` counts the stored ones too. A file the page sends becomes such a row when its `redz:files` promise resolves with the id the server gave it. Below, the contract is in use by an invoice, and a paper added is sent and kept under the next id.

```html
<div class="upload" data-max-files="4">
	<input class="visually-hidden" id="upload-stored-input" type="file" name="papers[]" accept=".pdf,image/*" multiple tabindex="-1" aria-label="Papers">
	<ul class="upload-list" aria-label="Papers">
		<li data-file-stored="41"><input type="hidden" name="kept[]" value="41"><i class="bi bi-file-earmark-text" aria-hidden="true"></i><span data-file-name>contract.pdf</span><span class="invalid-feedback" data-file-error></span><button class="btn btn-xs btn-icon" type="button" data-file-remove aria-label="Remove contract.pdf"><i class="bi bi-x-lg" aria-hidden="true"></i></button></li>
		<li data-file-stored="42"><input type="hidden" name="kept[]" value="42"><i class="bi bi-file-earmark-image" aria-hidden="true"></i><span data-file-name>site-plan.png</span><span class="invalid-feedback" data-file-error></span><button class="btn btn-xs btn-icon" type="button" data-file-remove aria-label="Remove site-plan.png"><i class="bi bi-x-lg" aria-hidden="true"></i></button></li>
	</ul>
	<button class="upload-zone btn btn-sm" type="button"><i class="bi bi-plus-lg" aria-hidden="true"></i>Add papers</button>
	<template>
		<li>
			<i class="bi bi-file-earmark" aria-hidden="true"></i>
			<span data-file-name></span>
			<progress class="progress progress-xs"></progress>
			<span class="invalid-feedback" data-file-error></span>
			<button class="btn btn-xs btn-icon" type="button" data-file-remove><i class="bi bi-x-lg" aria-hidden="true"></i></button>
		</li>
	</template>
</div>
```

### An image field

`upload-image` on an upload of one image shows its image whole on white, `--redz-upload-image-height` tall - 12rem by default - with its remove button in the corner; the zone, a plain button here, replaces it. The `capture` of the input goes to the browser as it is.

```html
<div class="upload upload-image max-w-360">
	<input class="visually-hidden" id="upload-image-input" type="file" name="cover" accept="image/*" tabindex="-1" aria-label="Cover">
	<ul class="upload-list" aria-label="Cover">
		<li data-file-stored="7"><input type="hidden" name="cover_kept" value="7"><img data-file-preview src="img/lagoon.svg" alt=""><span data-file-name>lagoon.svg</span><span class="invalid-feedback" data-file-error></span><button class="btn btn-xs btn-icon" type="button" data-file-remove aria-label="Remove lagoon.svg"><i class="bi bi-x-lg" aria-hidden="true"></i></button></li>
	</ul>
	<button class="upload-zone btn btn-sm" type="button"><i class="bi bi-image" aria-hidden="true"></i>Choose another image</button>
	<template>
		<li>
			<img data-file-preview alt="">
			<span data-file-name></span>
			<progress class="progress progress-xs"></progress>
			<span class="invalid-feedback" data-file-error></span>
			<button class="btn btn-xs btn-icon" type="button" data-file-remove><i class="bi bi-x-lg" aria-hidden="true"></i></button>
		</li>
	</template>
</div>
```

## Keyboard and accessibility

- The zone is one real button: `Enter` or `Space` opens the browser's picker; the input itself stays out of the Tab order, and its words - which follow the browser's language, not the page's - never show.
- A refused or failed file says why in its row, at once; a removed file's focus goes to the next file's remove button, else to the zone.
- Each remove button is named after its file, from the page's text `remove`.

## Texts

| Key | For | In English |
| --- | --- | --- |
| `file.type` | A file the input's `accept` does not take; `{name}` is its name | {name} is not a kind of file this takes |
| `file.size` | A file over `data-max-size`; `{size}` the limit in the page's units | {name} is larger than {size} |
| `remove` | A file's remove button | Remove {label} |

## Tokens

From [`css/components/upload.css`](https://github.com/redz-tech/REDZ.UI/blob/main/css/components/upload.css), which declares no custom property of its own.

It reads the theme's `--redz-space-8`, `--redz-space-4`, `--redz-space-24`, `--redz-space-16`, `--redz-border-strong`, `--redz-radius-12`, `--redz-bg`, `--redz-text-muted`, `--redz-primary`, `--redz-bg-hover`, `--redz-border`, `--redz-upload-image-height`, `--redz-radius-8`, `--redz-bg-page`, `--redz-space-2`.
