Created
September 3, 2015 15:01
-
-
Save firegoby/960980d6e24efcb83c7a to your computer and use it in GitHub Desktop.
Meteor React Jcrop Slingshot Semantic UI Component - User locally crops an image and uploads direct to S3 via Slingshot with upload progress
This file contains hidden or 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
let stages = { | |
DISABLED: 'disabled', | |
ACTIVE: 'active', | |
SAVING: 'saving', | |
WARNING: 'warning', | |
ERROR: 'error', | |
SUCCESS: 'success' | |
} | |
let stage = new ReactiveVar(stages.DISABLED) | |
let uploader = new ReactiveVar(null) | |
let jcropAPI = null | |
let labels = {} | |
labels[stages.DISABLED] = '', | |
labels[stages.ACTIVE] = 'Uploading photo...', | |
labels[stages.SAVING] = 'Saving photo...', | |
labels[stages.WARNING] = "Your photo doesn't meet the image requirements, choose another", | |
labels[stages.ERROR] = 'An error occurred while uploading your photo', | |
labels[stages.SUCCESS] = 'Photo uploaded successfully!' | |
Cropper = React.createClass({ | |
propTypes: { | |
slingshot: React.PropTypes.string, // slingshot Directive *required | |
width: React.PropTypes.string, // upload width | |
height: React.PropTypes.string, // upload height | |
imageType: React.PropTypes.string, // image MIME type, compatible with canvas.toBlob() | |
imageQuality: React.PropTypes.number, // image quality for JPG or WEBP, number 0.0 to 1.0 | |
options: React.PropTypes.object, // Jcrop options | |
onError: React.PropTypes.func, // on upload error callback(error, downloadURL) | |
onSuccess: React.PropTypes.func, // on upload success callback(downloadURL) | |
setFilename: React.PropTypes.func //set upload filename callback(fileEvent) | |
}, | |
getDefaultProps() { | |
return { | |
width: "300", | |
height: "300", | |
imageType: 'image/png', | |
imageQuality: 1.0, | |
options: { | |
minSize: [200, 200], | |
aspectRatio: 1, | |
bgFade: true, | |
bgOpacity: 0.5, | |
boxWidth: 500, | |
boxHeight: 0, | |
setSelect: [ 50, 50, 150, 150 ] | |
}, | |
onError: function(error, upload) { | |
console.error('Error uploading', upload.xhr.response) | |
alert (error) | |
}, | |
onSuccess: function(downloadURL) { | |
console.log('Success uploading: ', downloadURL) | |
}, | |
setFilename: function(ev) { | |
return Meteor.userId() + '-' + Date.now() | |
} | |
} | |
}, | |
mixins: [ReactMeteorData], | |
getMeteorData() { | |
return { | |
color: stage.get() === stages.ACTIVE || stage.get() === stages.SAVING ? 'blue' : '', | |
label: labels[stage.get()], | |
loading: stage.get() === stages.ACTIVE || stage.get() === stages.SAVING ? 'loading' : '', | |
progress: this.updateProgress(uploader.get()) | |
} | |
}, | |
handleFileChange(ev) { | |
uploader.set() | |
stage.set(stages.DISABLED) | |
let self = this | |
let file = React.findDOMNode(this.refs.File).files[0] | |
let img = React.findDOMNode(this.refs.Photo) | |
let reader = new FileReader() | |
$(React.findDOMNode(this.refs.Step2)).slideDown() | |
reader.onload = function(ev) { | |
img.src = ev.target.result | |
img.onload = function() { | |
if (jcropAPI) jcropAPI.setImage(img.src) | |
let options = self.props.options | |
options.onChange = self.updatePreview | |
options.onSelect = self.updatePreview | |
$(img).Jcrop(options, function(){ | |
jcropAPI = this // store the jcrop API for reuse | |
}) | |
} | |
} | |
reader.readAsDataURL(file) | |
}, | |
handleFileChoose(ev) { | |
$(React.findDOMNode(this.refs.File)).click() | |
}, | |
handleFileUpload(ev) { | |
uploader.set() | |
stage.set(stages.DISABLED) | |
let self = this | |
let upload = new Slingshot.Upload(this.props.slingshot) | |
let canvas = React.findDOMNode(this.refs.Preview) | |
let blob = null | |
canvas.toBlob(function(newBlob) { | |
let ext | |
switch(self.props.imageType) { | |
case 'image/jpeg': ext = 'jpg'; break | |
case 'image/webp': ext = 'webp'; break | |
default: ext = 'png' | |
} | |
blob = newBlob | |
blob.name = `${self.props.setFilename(ev)}.${ext}` | |
}, this.props.imageType, this.props.imageQuality) | |
stage.set(stages.ACTIVE) | |
upload.send(blob, function (error, downloadURL) { | |
if (error) { | |
stage.set(stages.ERROR) | |
self.props.onError(error, upload) | |
} else { | |
stage.set(stages.SUCCESS) | |
self.props.onSuccess(downloadURL) | |
} | |
}) | |
uploader.set(upload) | |
}, | |
updatePreview(ev) { | |
if (parseInt(ev.w) > 0) { | |
let img = React.findDOMNode(this.refs.Photo) | |
let canvas = React.findDOMNode(this.refs.Preview) | |
canvas.width = this.props.width | |
canvas.height = this.props.height | |
let context = canvas.getContext('2d') | |
context.drawImage(img, ev.x, ev.y, ev.w, ev.h, 0, 0, canvas.width, canvas.height) | |
} | |
}, | |
updateProgress(upload) { | |
if (upload) { | |
let percent = Math.round(upload.progress() * 100) || 0 | |
if (percent === 100 && stage.get() !== stages.SUCCESS) stage.set(stages.SAVING) | |
return percent | |
} | |
return 0 | |
}, | |
componentDidMount() { | |
if (!HTMLCanvasElement.prototype.toBlob) { | |
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { | |
value: function (callback, type, quality) { | |
let binStr = atob( this.toDataURL(type, quality).split(',')[1] ) | |
let len = binStr.length | |
let arr = new Uint8Array(len) | |
for (var i=0; i<len; i++ ) { | |
arr[i] = binStr.charCodeAt(i) | |
} | |
callback( new Blob( [arr], {type: type || 'image/png'} ) ) | |
} | |
}) | |
} | |
}, | |
render() { | |
let previewStyle = { | |
width: this.props.width / 2, | |
height: this.props.height / 2, | |
overflow: 'hidden' | |
} | |
return ( | |
<form encType="multipart/form-data" method="post" action="" onSubmit={ev => ev.preventDefault()}> | |
<h1 className="header">Edit your Profile Picture</h1> | |
<input type="file" ref="File" onChange={this.handleFileChange} style={{display: 'none'}}/> | |
<button className="ui primary button" onClick={this.handleFileChoose}>Choose a photo</button> | |
<hr className="ui hidden divider" /> | |
<div ref="Step2" style={{display: 'none'}}> | |
<h3 className="header">Please select an area for your avatar</h3> | |
<hr className="ui hidden divider" /> | |
<img ref="Photo" /> | |
<hr className="ui hidden divider" /> | |
<canvas ref="Preview" style={previewStyle}></canvas> | |
<hr className="ui hidden divider" /> | |
<div className={`ui progress ${stage.get()} ${this.data.color}`} data-percent={this.data.progress}> | |
<div className="bar" style={{transitionDuration: '300ms', width: `${this.data.progress}%`}}> | |
<div className="progress">{this.data.progress}%</div> | |
</div> | |
<div className="label">{this.data.label}</div> | |
</div> | |
<button className="ui primary button" ref="Upload" onClick={this.handleFileUpload}>Upload your Photo</button> | |
</div> | |
</form> | |
) | |
} | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment