Skip to content

Instantly share code, notes, and snippets.

@krhoyt
Created May 18, 2017 09:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save krhoyt/3b6d76af63194052b98e07d970c53d5f to your computer and use it in GitHub Desktop.
Save krhoyt/3b6d76af63194052b98e07d970c53d5f to your computer and use it in GitHub Desktop.
Animated File Upload Button
var express = require( 'express' );
var fs = require( 'fs' );
var multer = require( 'multer' );
var path = require( 'path' );
var randomstring = require( 'randomstring' );
// Router
var router = express.Router();
// Upload storage options
// Unique name with extension
var storage = multer.diskStorage( {
destination: 'uploads',
filename: function( req, file, cb ) {
cb( null, randomstring.generate() + '.jpg' );
}
} );
// Upload handler
var upload = multer( {
storage: storage
} );
// Get specific image
router.get( '/upload/:path', function( req, res ) {
res.sendFile( path.resolve( __dirname + '/../uploads/' + req.params.path ) );
} );
// Image upload
router.post( '/upload', upload.single( 'attachment' ), function( req, res ) {
// Get name from path
var parts = req.file.path.split( '/' );
var file = parts[1].split( '.' )[0];
// Respond with name
res.json( {
name: file
} );
} );
// Test
router.get( '/test', function( req, res ) {
res.json( {facial: 'Image management.'} );
} );
// Export
module.exports = router;
<html>
<head>
<title>Progress</title>
<link href="favicon.ico" rel="icon">
<!-- Google Fonts -->
<!-- Roboto -->
<link href="https://fonts.googleapis.com/css?family=Roboto:400,700" rel="stylesheet">
<!-- Styles -->
<link href="style/progress.css" rel="stylesheet" type="text/css">
</head>
<body>
<!-- File upload -->
<input id="upload" type="file" accept=".jpg,.jpeg">
<label for="upload">
<!-- SVG content in label -->
<svg width="56" height="56">
<!-- Background -->
<circle cx="28" cy="28" r="28" fill="red"/>
<!-- Icon -->
<!-- From Material Design -->
<path
class="icon"
d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z"
fill="white"
transform="translate( 15, 15 )"/>
<!-- Animated completion indicator -->
<!-- Pie slice -->
<path
class="pie"
d="M 28 28 L 28 0 A 28 28 1 0 1 28 0 z"
fill="red"
opacity="0"/>
<!-- Numeric completion indicator -->
<text
x="28"
y="28"
text-anchor="middle"
fill="white"
font-size="14"
font-weight="700"
dominant-baseline="central"
opacity="0">0%</text>
</svg>
</label>
<!-- Full size image -->
<img id="full" style="visibility: hidden; position: absolute;">
<!-- Progress -->
<script src="script/progress.js"></script>
<!-- Application -->
<script>
let button = new ProgressButton( 'label' );
button.root.addEventListener( ProgressButton.COMPLETE, evt => {
console.log( evt );
let image = document.querySelector( '#full' );
image.src = '/api/image/upload/' + evt.detail.name + '.jpg';
} );
</script>
</body>
</html>
class ProgressButton {
constructor( path ) {
this.root = document.querySelector( path );
this.root.addEventListener( 'click', evt => this.doClick( evt ) );
this.svg = this.root.querySelector( 'svg' );
// Hidden file form field
this.file = document.querySelector( '#' + this.root.getAttribute( 'for' ) );
this.file.addEventListener( 'change', evt => this.doFile( evt ) );
// Reference parts
this.circle = this.svg.querySelector( 'circle' );
this.complete = this.svg.querySelector( 'text' );
this.icon = this.svg.querySelector( '.icon' );
this.pie = this.svg.querySelector( '.pie' );
// Complete (0 - 100)
this.percent = 0;
}
// Animate to current completion
progress() {
// Completed
// Reset to ready
if( this.percent == 100 ) {
this.circle.setAttributeNS( null, 'opacity', 1 );
this.pie.setAttributeNS( null, 'opacity', 0 );
this.complete.setAttributeNS( null, 'opacity', 0 );
this.icon.setAttributeNS( null, 'opacity', 1 );
this.percent = 0;
// Allow selection of same file
// Set value to blank
// Same file will look like a change
this.file.value = '';
}
// Completion arc calculator
// http://stackoverflow.com/questions/5736398/how-to-calculate-the-svg-path-for-an-arc-of-a-circle
let angle = ( this.percent / 100 ) * 360;
let radians = ( angle - 90 ) * Math.PI / 180;
let arc = angle <= 180 ? '0' : '1';
let slice = {
x: 28 + ( 28 * Math.cos( radians ) ),
y: 28 + ( 28 * Math.sin( radians ) )
};
let d = [
'M', 28, 28,
'L', 28, 0,
'A', 28, 28, 1, arc, 1, slice.x, slice.y,
'z'
].join( ' ' );
// Update completion pie
// Update numeric indicator
this.pie.setAttributeNS( null, 'd', d );
this.complete.innerHTML = Math.round( this.percent ) + '%';
}
doClick( evt ) {
// Hold up on file selection
evt.preventDefault();
// Not currently uploading
// Proceed
if( this.icon.getAttributeNS( null, 'opacity' ) != '0' ) {
this.file.click();
}
}
// File selected
doFile( evt ) {
// No file selected
// Abort
if( evt.target.files.length == 0 ) {
return;
}
// Set state for upload reporting
this.icon.setAttributeNS( null, 'opacity', 0 );
this.circle.setAttributeNS( null, 'opacity', 0.50 );
this.pie.setAttributeNS( null, 'opacity', 1 );
this.complete.innerHTML = '0%';
this.complete.setAttributeNS( null, 'opacity', 1 );
// Instantiate
// Hook events if needed
if( this.xhr == null ) {
this.xhr = new XMLHttpRequest();
this.xhr.addEventListener( 'load', evt => this.doLoad( evt ) );
this.xhr.upload.addEventListener( 'progress', evt => this.doProgress( evt ) );
}
// File to upload
let data = new FormData();
data.append( 'attachment', evt.target.files[0] );
// Send to API
this.xhr.open( 'POST', '/api/image/upload', true );
this.xhr.send( data );
}
// Upload completed
// Response recieved
doLoad( evt ) {
let data = JSON.parse( this.xhr.responseText );
console.log( data );
let event = new CustomEvent( ProgressButton.COMPLETE, {detail: data} );
this.root.dispatchEvent( event );
}
// Upload in progress
// Reflect values in visualization
doProgress( evt ) {
this.percent = ( evt.loaded / evt.total ) * 100;
this.progress();
}
}
ProgressButton.COMPLETE = 'progress_complete';
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment