FileDrop
Drop files or click to choose them: a real file input that a form posts, listing names and sizes.
Demo
Drop files here or choose themimage/*
PNG or JPEG, up to 10 MB each.
Drop files here or choose them.pdf, .docx, .txt
Nothing chosen yet.
Uploads are closed
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/file_drop.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/file_drop.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/file_drop.qwe
component FileDropDemoLive() { state total = 0 <div.demo-stack> <FileDrop name="fd-docs" label="Documents" accept=".pdf,.docx,.txt" multiple={true} on_change={(fs) => { total = sum(fs | map((f) => f.size)) }}/> <p.demo-note>{total > 0 ? `${file_drop_size(total)} in all.` : "Nothing chosen yet."}</p> </div>}view FileDropDemo() = <div.demo-stack.demo-narrow> <FileDrop name="fd-photos" label="Photos" accept="image/*" multiple={true} hint="PNG or JPEG, up to 10 MB each." required={true}/> <FileDropDemoLive/> <FileDrop name="fd-off" label="Contract" text="Uploads are closed" disabled={true}/> </div>Source
ui/file_drop.qwe
// FileDrop: a place to drop files, or click to choose them, that lists what was chosen.// The zone is a real <input type="file"> stretched over it, so a dropped file is// chosen as a clicked one is, and a <form method="post" enctype="multipart/form-data">// sends it under `name` (see Uploads in the guide).//// <form method="post" action="/upload" enctype="multipart/form-data">// <FileDrop name="photos" label="Photos" accept="image/*" multiple={true}/>// </form>//// name ("file"), label, hint, error, required, disabled, field (the input's id,// "file-drop-" + name by default) as for every field. accept: the kinds of file// taken ("image/*", ".pdf,.docx"). multiple: more than one file.// text: what the zone says ("Drop files here or choose them").// on_change(files): inside one of your components, called with the files chosen,// as maps of { name, size } (size in bytes).import ui/theme.qwe use ui_clslet file_drop_css = css`.ui-file-drop { display: grid; gap: 6px; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-file-drop-label { font-size: 13px; font-weight: 500; color: var(--ui-fg); }.ui-file-drop-req { margin-left: 2px; color: var(--ui-danger); }.ui-file-drop-hint { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-file-drop-error { margin: 0; font-size: 13px; color: var(--ui-danger); }.ui-file-drop-zone { position: relative; display: grid; justify-items: center; gap: 6px; padding: 26px 16px; text-align: center; border: 1.5px dashed var(--ui-strong); border-radius: var(--ui-radius); background: var(--ui-subtle); color: var(--ui-muted); transition: border-color .15s, background .15s, box-shadow .15s;}.ui-file-drop-zone input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }.ui-file-drop-zone:hover { border-color: var(--ui-accent); }.ui-file-drop-zone:has(input:focus-visible) { border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-file-drop-over { border-color: var(--ui-accent); border-style: solid; background: var(--ui-accent-soft); }.ui-file-drop-zone svg { width: 28px; height: 28px; color: var(--ui-accent); }.ui-file-drop-text { color: var(--ui-fg); font-weight: 500; }.ui-file-drop-text u { color: var(--ui-accent); text-underline-offset: 3px; }.ui-file-drop-sub { font-size: 13px; }.ui-file-drop-invalid { border-color: var(--ui-danger); }.ui-file-drop:has(input:disabled) .ui-file-drop-zone { opacity: .55; }.ui-file-drop-zone:has(input:disabled), .ui-file-drop-zone input:disabled { cursor: not-allowed; border-color: var(--ui-strong); }.ui-file-drop-list { display: grid; gap: 6px; margin: 0; padding: 0; list-style: none; }.ui-file-drop-list li { display: flex; align-items: center; gap: 10px; padding: 8px 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); font-size: 13px;}.ui-file-drop-list svg { flex: none; width: 16px; height: 16px; color: var(--ui-muted); }.ui-file-drop-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.ui-file-drop-size { flex: none; color: var(--ui-muted); font-variant-numeric: tabular-nums; }@media (prefers-reduced-motion: reduce) { .ui-file-drop-zone { transition: none; } }`// A size in bytes as people read it: 812 B, 14.2 KB, 3.1 MB.fn file_drop_size(n: num) -> str { if n < 1024 { return `${n} B` } if n < 1024 * 1024 { return `${round(n / 1024, 1)} KB` } if n < 1024 * 1024 * 1024 { return `${round(n / 1024 / 1024, 1)} MB` } `${round(n / 1024 / 1024 / 1024, 1)} GB`}component FileDrop(name: str = "file", label: str? = null, hint: str? = null, error: str? = null, required: bool = false, disabled: bool = false, field: str? = null, accept: str? = null, multiple: bool = false, text: str = "Drop files here or choose them", on_change = null, class: str? = null) { style global file_drop_css state files = [] state over = false let fid = field ?? `file-drop-${name}` let described = ui_cls([hint != null && `${fid}-hint`, error != null && `${fid}-error`]) <div class={ui_cls(["ui-file-drop", class])}> {label != null && <label.ui-file-drop-label for={fid}> {label}{required && <span.ui-file-drop-req aria-hidden="true">*</span>} </label>} <div class={ui_cls(["ui-file-drop-zone", over && "ui-file-drop-over", error != null && "ui-file-drop-invalid"])}> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 15V4M7.5 8.5L12 4l4.5 4.5"/><path d="M4 14v4a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-4"/></svg> <span.ui-file-drop-text>{text}</span> {accept != null && <span.ui-file-drop-sub>{replace(accept, ",", ", ")}</span>} <input type="file" id={fid} name={name} accept={accept} multiple={multiple} required={required} disabled={disabled} aria-label={label == null ? text : null} aria-invalid={error != null ? "true" : null} aria-describedby={described != "" ? described : null} @dragenter={() => { over = true }} @dragleave={() => { over = false }} @drop={() => { over = false }} @change={(e) => { let fl = e.target.files files = for i in range(0, fl.length) => { name: fl[i].name, size: fl[i].size } on_change && on_change(files) }}/> </div> {len(files) > 0 && <ul.ui-file-drop-list aria-label="Chosen files"> {for f in files => <li> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" aria-hidden="true"><path d="M4 1.5h5.5L13 5v9.5H4z"/><path d="M9.5 1.5V5H13"/></svg> <span.ui-file-drop-name>{f.name}</span> <span.ui-file-drop-size>{file_drop_size(f.size)}</span> </li>} </ul>} {hint != null && <p.ui-file-drop-hint id={`${fid}-hint`}>{hint}</p>} {error != null && <p.ui-file-drop-error id={`${fid}-error`} role="alert">{error}</p>} </div>}Related
AvatarUploadA round picture with Change and Remove, showing the picture picked before it is sent, on a real file input.6 views · 6 downloadsFileUploadDrop or choose files: kinds, sizes and thumbnails listed, limits checked per file, removable, with upload progress.5 views · 6 downloadsProfileFormAvatar, name, username, bio and website in the usual layout: a plain form posting to your route, shown again with errors.5 views · 8 downloadsAddressFormStreet, city, region, postcode and country in the usual layout, with the autocomplete names browsers fill from.5 views · 7 downloadsAmountInputReads 1.5k, 2m, 1,234.50, 1.234,50, $12 and EUR 40, decides which mark groups, shows the normalised amount, flags what could go two ways, and sends a real decimal.3 views · 8 downloadsAuthFormSign in, sign up, forgot and reset password in one form that switches in place, each posting to its own route.7 views · 4 downloads
Thread
Thread 0 posts
On FileDrop: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.