Last active
December 30, 2019 17:35
-
-
Save davidbgk/b455d4c3744737c30c6874e4cf8783a3 to your computer and use it in GitHub Desktop.
Upload images form with drag & drop and previews and progress upload
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html><!-- This is a valid HTML5 document. --> | |
<!-- Screen readers, SEO, extensions and so on. --> | |
<html lang=fr> | |
<!-- Has to be within the first 1024 bytes, hence before the <title> | |
See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset --> | |
<meta charset=utf-8> | |
<!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 --> | |
<!-- The viewport meta is quite crowded and we are responsible for that. | |
See: https://codepen.io/tigt/post/meta-viewport-for-2015 --> | |
<meta name=viewport content="width=device-width,minimum-scale=1,initial-scale=1,shrink-to-fit=no"> | |
<!-- Avoid indexation of the content as it is private. --> | |
<meta name="robots" content="noindex, nofollow"> | |
<!-- Prevent access via referrers, only the domain will be available. --> | |
<meta content="origin-when-cross-origin" name="referrer"> | |
<!-- Required to make a valid HTML5 document. --> | |
<title>Upload progress form</title> | |
<!-- Lightest blank gif, avoids an extra query to the server. --> | |
<link rel=icon href="data:;base64,iVBORw0KGgo="> | |
<style type="text/css"> | |
form { | |
max-width: 30rem; | |
margin: 1rem auto; | |
font-size: 2rem; | |
text-align: center; | |
} | |
input[type="file"] { | |
width: 0.1px; | |
height: 0.1px; | |
opacity: 0; | |
overflow: hidden; | |
position: absolute; | |
z-index: -1; | |
} | |
input[type="file"] + label { | |
font-size: 1.25em; | |
font-weight: 700; | |
color: white; | |
background-color: black; | |
display: inline-block; | |
} | |
input[type="file"]:focus + label, | |
input[type="file"] + label:hover { | |
background-color: red; | |
} | |
input[type="file"] + label { | |
cursor: pointer; /* "hand" cursor */ | |
} | |
input[type="file"]:focus + label { | |
outline: thin dotted; | |
outline: -webkit-focus-ring-color auto 5px; | |
} | |
input[type="file"]:focus-within + label { | |
outline: thin dotted; | |
outline: -webkit-focus-ring-color auto 5px; | |
} | |
input[type="file"] + label * { | |
pointer-events: none; | |
} | |
output img { | |
max-width: 30rem; | |
} | |
output.oversized { | |
background: red; | |
height: 4rem; | |
padding: 1rem 2rem; | |
} | |
canvas { | |
border: 5px solid black; | |
margin: 2rem auto; | |
} | |
canvas.hidden { | |
display: none; | |
} | |
canvas:hover, | |
canvas.active { | |
border-color: red; | |
} | |
</style> | |
<form action="/" method="post" enctype="multipart/form-data"> | |
<fieldset> | |
<input | |
type="file" name="files" id="files" | |
accept="image/*" multiple | |
data-max-size-kb="500"> | |
<label for="files"> | |
Click to upload image(s) or drag & drop below | |
</label> | |
<output></output> | |
<canvas></canvas> | |
</fieldset> | |
<input type="submit" name="submit"> | |
</form> | |
<script type="text/javascript"> | |
/* | |
Resources: | |
- https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications | |
- https://tympanus.net/codrops/2015/09/15/styling-customizing-file-inputs-smart-way/ | |
- https://eloquentjavascript.net/18_http.html | |
*/ | |
;(function handleImageSelection( | |
inputSelector, | |
outputSelector, | |
dropzoneSelector | |
) { | |
'use strict' | |
const input = document.querySelector(inputSelector) | |
const output = document.querySelector(outputSelector) | |
const dropzone = document.querySelector(dropzoneSelector) | |
function handleImages(images) { | |
Array.from(images).forEach(image => { | |
const imageName = image.name | |
const imageSize = image.size | |
const isOversized = | |
Math.round(imageSize / 1024) > Number(input.dataset.maxSizeKb) | |
if (isOversized) { | |
output.classList.add('oversized') | |
output.innerText = `Oversized!` | |
input.value = '' | |
return | |
} else { | |
output.classList.remove('oversized') | |
dropzone.classList.add('hidden') | |
} | |
const figure = document.createElement('figure') | |
const img = document.createElement('img') | |
img.file = image // Required for future upload, fragile? | |
img.src = window.URL.createObjectURL(image) | |
img.onload = event => window.URL.revokeObjectURL(event.target.src) | |
img.alt = 'Picture preview' | |
figure.appendChild(img) | |
const figcaption = document.createElement('figcaption') | |
figcaption.innerText = imageName | |
figure.appendChild(figcaption) | |
output.appendChild(figure) | |
}) | |
} | |
input.addEventListener( | |
'change', | |
event => { | |
event.stopPropagation() | |
event.preventDefault() | |
handleImages(event.target.files) | |
}, | |
false | |
) | |
dropzone.addEventListener( | |
'drop', | |
event => { | |
event.stopPropagation() | |
event.preventDefault() | |
handleImages(event.dataTransfer.files) | |
}, | |
false | |
) | |
})('input[type="file"]', 'output', 'canvas') | |
;(function uploadImageWithProgress(formSelector, imagesSelector) { | |
'use strict' | |
function request(url, options = {}, onProgress) { | |
// See https://github.com/github/fetch/issues/89#issuecomment-256610849 | |
return new Promise((resolve, reject) => { | |
const xhr = new XMLHttpRequest() | |
xhr.open(options.method || 'get', url) | |
for (let header in options.headers || {}) | |
xhr.setRequestHeader(header, options.headers[header]) | |
xhr.onload = event => resolve(event.target.responseText) | |
xhr.onerror = reject | |
if (xhr.upload && onProgress) xhr.upload.onprogress = onProgress | |
xhr.send(options.body) | |
}) | |
} | |
function displayPercentage(event) { | |
if (event.lengthComputable) { | |
const percentage = Math.round((event.loaded * 100) / event.total) | |
console.log(percentage) | |
} | |
} | |
const form = document.querySelector(formSelector) | |
form.addEventListener( | |
'submit', | |
event => { | |
event.stopPropagation() | |
event.preventDefault() | |
const images = document.querySelectorAll(imagesSelector) | |
Array.from(images).forEach(image => { | |
const options = { | |
method: event.target.method, | |
body: image.file | |
} | |
console.log(`Uploading "${image.file.name}"`) | |
request(event.target.action, options, displayPercentage) | |
.then(console.log) | |
.catch(console.log.bind(console)) | |
}) | |
}, | |
false | |
) | |
})('form', 'output img') | |
;(function highlightOnDragOrFocus(targetSelector, className) { | |
'use strict' | |
function addEventListenerMulti(element, events, fn) { | |
/* https://stackoverflow.com/a/45096932 */ | |
events | |
.split(' ') | |
.forEach(event => element.addEventListener(event, fn, false)) | |
} | |
const target = document.querySelector(targetSelector) | |
if (target) { | |
addEventListenerMulti(target, 'dragenter dragover focus', event => { | |
event.stopPropagation() | |
event.preventDefault() | |
target.classList.add(className) | |
}) | |
addEventListenerMulti(target, 'dragleave blur', event => { | |
event.stopPropagation() | |
event.preventDefault() | |
target.classList.remove(className) | |
}) | |
} | |
})('canvas', 'active') | |
;(function openFileSelectorOnDropzoneClick(targetSelector, inputSelector) { | |
'use strict' | |
const target = document.querySelector(targetSelector) | |
const input = document.querySelector(inputSelector) | |
target.addEventListener('click', event => input.click(), false) | |
})('canvas', 'input[type="file"]') | |
</script> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment