Skip to content

Instantly share code, notes, and snippets.

@thekid
Last active January 26, 2024 19:52
Show Gist options
  • Save thekid/5a0f1d26e3be520397a2d8e53a901ebf to your computer and use it in GitHub Desktop.
Save thekid/5a0f1d26e3be520397a2d8e53a901ebf to your computer and use it in GitHub Desktop.
HTMX & Vanilla JS & XP Upload, including drag & drop
{
"name": "thekid/upload",
"description": "Upload example",
"type": "project",
"require": {
"xp-forge/handlebars-templates": "^3.3",
"xp-forge/frontend": "^6.0"
}
}
<?php
use util\cmd\Console;
use web\Application;
use web\frontend\{Frontend, Handlebars, Get, Post, View, Request};
class Upload extends Application {
public function routes() {
$impl= new class() {
#[Get]
public function list() {
return View::named('upload');
}
#[Post('/upload')]
public function upload(#[Request] $req) {
$uploaded= [];
if ($multipart= $req->multipart()) {
foreach ($multipart->files() as $name => $file) {
Console::write(' Upload ', $file, ' [');
$transferred= $previous= 0;
while ($file->available()) {
$transferred+= strlen($file->read());
if (($transferred - $previous) > 262144) {
$display= sprintf('%.1f', $transferred / 1048576);
Console::write("{$display} MB]\033[".(strlen($display) + 4)."D");
$previous= $transferred;
}
}
$uploaded[$name]= sprintf('%.1f', $transferred / 1048576);
Console::writeLine("{$uploaded[$name]} MB]\033[K");
}
}
return View::named('upload')->fragment('success')->with(['uploaded' => $uploaded]);
}
};
return new Frontend($impl, new Handlebars('.'));
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Upload</title>
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@300&display=swap" rel="stylesheet">
<style type="text/css">
* {
font-family: 'Lato', sans-serif;
padding: 0;
margin: 0;
}
h1, h2, h3 {
line-height: 1.2;
}
p {
line-height: 1.6;
}
ul {
margin: 1.5rem;
}
tt {
font-family: 'Cascadia Code PL', monospace;
}
main {
padding: 1rem;
}
form input, form button {
font-size: 1rem;
}
progress {
appearance: none;
display: inline-block;
width: 10rem;
height: 1.25rem;
border: 0;
visibility: hidden;
}
progress::-webkit-progress-bar {
background: lightgray;
}
progress::-webkit-progress-value {
background: #06c;
transition: width .25s ease-in-out;
}
#drop {
background: #efefef;
border: .125rem dashed transparent;
height: 2rem;
display: flex;
align-items: center;
cursor: pointer;
margin-block: .25rem;
}
#drop.hovering {
border-color: black;
}
#drop input[type='file'] {
display: none;
}
</style>
</head>
<body {{#with request.values.token}}hx-headers='{"X-CSRF-Token": "{{.}}"}'{{/with}}>
<main class="segments">
<h1>Upload files</h1>
<form id="form" hx-encoding="multipart/form-data" hx-post="/upload" hx-target="#message">
<div id="drop">
<span class="files"></span>
<input type="file" name="file" multiple>
</div>
<button>Upload</button>
<progress id="progress" value="0" max="100"></progress>
</form>
<div id="message"></div>
</main>
{{#*inline "success"}}
{{#with uploaded}}
<ul>
{{#each .}}
<li>File <tt>{{@key}}</tt> (size: {{.}} MB) uploaded!</li>
{{/each}}
</ul>
{{else}}
No file uploaded!
{{/with}}
{{/inline}}
<script src="https://unpkg.com/htmx.org@1.9.10/dist/htmx.min.js"></script>
<script type="module">
const $progress = document.getElementById('progress');
const $form = document.getElementById('form');
const $drop = document.getElementById('drop');
const $files = $form.elements['file'];
const filesChanged = e => {
let list = '';
for (const file of $files.files) {
list += ', ' + file.name + ' (' + file.type + ')';
}
$drop.querySelector('.files').innerText = list.substring(2);
};
$files.addEventListener('change', filesChanged);
$drop.addEventListener('drop', e => {
e.preventDefault();
$drop.classList.remove('hovering');
// If `Shift` key was pressed, append file
if (e.shiftKey) {
const list = new DataTransfer();
for (const file of $files.files) {
list.items.add(file);
}
for (const file of e.dataTransfer.files) {
list.items.add(file);
}
$files.files = list.files;
} else {
$files.files = e.dataTransfer.files;
}
filesChanged();
});
$drop.addEventListener('click', e => {
$files.click()
});
$drop.addEventListener('dragover', e => {
e.preventDefault();
});
$drop.addEventListener('dragenter', e => {
e.preventDefault();
$drop.classList.add('hovering');
});
$drop.addEventListener('dragleave', e => {
e.preventDefault();
$drop.classList.remove('hovering');
});
$form.addEventListener('htmx:beforeRequest', e => {
$progress.style.visibility = 'visible';
});
$form.addEventListener('htmx:afterRequest', e => {
setTimeout(() => {
$progress.style.visibility = 'hidden';
$progress.setAttribute('value', 0);
}, 1000);
});
$form.addEventListener('htmx:xhr:progress', e => {
if (e.detail.lengthComputable) {
$progress.setAttribute('value', e.detail.loaded / e.detail.total * 100);
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment