# File upload, several files

A dashed drop zone with a "Choose files" button that takes several files at once. Every dropped or chosen file gets its own
row — name, size, a progress bar and its state (Waiting, Uploading…, Uploaded, Failed) — and the line under the rows counts
them: "3 of 3 uploaded". Files go one after the other in the order they came; files dropped while others are still uploading
wait in their row and follow. A failed file does not stop the others. Live demo: https://components.hybriel.worldapi.org/files-upload

**Two modes** (`mode`): **upload at once** (the default, `mode = "upload"`, all of the above) and **pick first, send later**
(`mode = "pick"`, ticket #18, for chat attachments — HyperConductor hycon#46): dropped, chosen, pasted (Ctrl+V) or photographed
files are only collected; the host sends them with its own message and empties the list. Live demo:
https://components.hybriel.worldapi.org/files-upload/pick

## Files
Copy the folder `components/files-upload/`: `files-upload.hl` is the component (both modes; it carries its own `Style`). `demo.hl`
and `demo-pick.hl` are this site's demo pages, not needed in an app.

## Use it in an app

Copy the folder `components/files-upload/` (`files-upload.hl`; it carries its own `Style`, colours are semantic tokens with a
black/white/grey fallback, the one border is the drop zone's dashed one), compose it, and write the face that stores a file:

```
import FilesUpload from './files-upload.hl'

View { FilesUpload { } }

on server uploadFile(file) {
	// file.name, file.type, file.size, file.bytes — store it where you want (called once per file)
	writeFile('storage/uploads/' + name, file.bytes)
	return { ok = true }
}
```

- The component sends each file with `emit server uploadFile(file)`; the framework carries it over HTTP in 1 MiB parts with
  progress (hybriel#94). Pure Hybriel: no `.js`, no network calls of its own.
- The face decides where the file goes and how big it may be (`uploadMax` of the WebFramework; this site: 5 MiB). Answer anything
  but `null` when it is stored; `null`, an error or a file over `uploadMax` shows "Failed" in that row.
- Face names are global to the app (`uploadFile`; the one-file upload uses `saveFile`): one face per app for each.
- `dropText`, `chooseText` are the texts. One component per page (composed members are the page's state).
- Progress comes from `on client uploadProgress`, shown by the component itself in the row being sent.
- A **Ctrl+V** of files (a screenshot, a copied image or file) on the focused drop zone uploads them like a drop. A text paste does nothing.
- `maxSize` (bytes, default 0 = none): a bigger file gets its row with "Too big — at most 2.0 MB" and is not sent (the app's
  `uploadMax` still holds for the rest).

## Pick first, send later (`mode = "pick"`)

Files are **collected, not sent**: an image shows as a thumbnail, any other file as a row with its kind ("PDF"), name and size,
each with a **✕** that takes it away. A line under them says "3 files · 1.2 MB". **The host owns the list** — the pattern of the
Comments block (hybriel#87): it binds its list into `files`, hears every change through the function `onChange`, and at its Send
passes the list to its own face and empties it. **Its input area goes inside the component** (the slot), and everything is
**one box**: the host's field on top, the picked files right under it, and a footer with the small "Choose files" (and, on a touch
screen, "Camera") button at the left and the host's own buttons — its Send — at the right end. A drop or a paste (Ctrl+V)
anywhere in the box is caught, while text pastes and text drags stay in the field. The host's `<form>` dissolves into the box
(`display: contents`): its first field (a `textarea` or an `md-editor`) is the top row, every other direct child of the form
(a button, a button group) goes to the right end of the footer.

### Full host example: a chat input (text + attachments + Send)
```
import FilesUpload from './files-upload/files-upload.hl'

Hybrid attached = []                     // the collected files (browser Files), owned by this page
Hybrid messages = []
picked = (list) => { attached = list }   // the component tells every change: a pick, a paste, a ✕

View {
	chat {
		ol { for (m of messages) { li { m.text } } }
		FilesUpload { mode = "pick"  files = attached  onChange = picked  maxSize = 10485760  accept = "image/*,.pdf,.txt,.md"
			form { on submit(e) { emit chatSubmit(e) }
				textarea { name = "text"  placeholder = "Write a message…" }
				button { type = "submit"  "Send" }
			}
		}
	}
}

on chatSubmit(e) {
	e.preventDefault()
	form = e.target
	text = form.querySelector('textarea').value
	if (text.trim() == '' && attached.length == 0) { return null }
	message = emit server chatSend(text, attached)   // the framework uploads every file first (hybriel#94), then the face runs
	if (message == null) { return null }             // refused (e.g. a file over uploadMax): the files stay, the user can retry
	messages.push(message)
	attached = []                                    // clears the component's rows
	form.reset()
}

on server chatSend(text, files) {
	// files: a list of { name, type, size, bytes } in the order shown; store them where you want
	for (f of files) { writeFile('storage/attachments/' + safeName(f.name), f.bytes) }
	return { text = text  files = files.length }
}
```

### Interface
| Member | Meaning |
|---|---|
| `mode` | `"pick"` (default `"upload"`) |
| `files` | **in**: the host's list of browser `File`s, shown in this order (bind the host's member; `[]` = no rows) |
| `onChange(list)` | **out**: called with the NEW list after every drop, choose, paste, photo and ✕; the host stores it (`attached = list`) — without it nothing is collected |
| `maxSize` | bytes, 0 = none: a bigger file gets a row "Too big — at most …" with its own ✕, and is not handed to the host |
| `accept` | the "Choose files" input's `accept` (default all files) |
| texts | `pickText` (the tooltip of "Choose files"), `chooseText` ("Choose files"), `cameraText` ("Camera"), `removeText` ("Remove", the ✕'s label: "Remove photo.png"), `tooBigText` ("Too big — at most") |

- **Send** = the host's own face with the list as an argument: `emit server chatSend(text, attached)`. A list of Files in an emit is
  uploaded by the framework in 1 MiB parts before the face runs; the face gets `{ name, type, size, bytes }` for each. **Clear** =
  `attached = []`. Nothing is sent by the component itself; it never calls a face in this mode.
- The refused rows belong to the last pick: a new pick replaces them, their ✕ dismisses one, and they go when the host empties a
  list that had files (after its Send).
- **Phone:** "Choose files" offers the phone's own choice (gallery, camera, files); on a touch screen (`pointer: coarse`) a
  **Camera** button (`accept="image/*" capture="environment"`) opens the camera straight away. Every button is 44 × 44 px or more.
- Thumbnails are object URLs of the Files (pure Hybriel: `window.URL.createObjectURL`), given back (`revokeObjectURL`) once drawn.

## Markup and states

`<files-upload>` holds `<drop-zone>` (class `over` while files are dragged over; the hidden `<input type="file" multiple>` in
`label.choose`), `<ul class="files">` with one `<li>` per file (`strong.name`, `span.size`, `<progress>`, `<output class="state">`;
class `waiting`, `busy`, `done` or `failed`) and `<output class="summary">`. Class `busy` on `<files-upload>` while it sends.

Pick mode: `<files-upload>` > `<pick-area>` (class `over` on `<files-upload>` while files are dragged over it) with
`<ul class="files picked">` — `li.picked[data-i]` (`img.thumb` or `span.kind`, `<file-info>` with `strong.name` + `span.size`,
`button.remove`) and `li.refused` (`span.kind` "!", `<file-info>` with `strong.name`, `span.size`, `output.state`, `button.remove`) —,
`<output class="summary">`, the host's content (first, in the DOM), and the footer's `label.choose` and `label.camera` (each holding its hidden input; there is no drop zone in pick mode — the whole box takes the drop).
