Last active
May 14, 2019 06:19
-
-
Save ikawka/1234141fa431da6b833434b13a09c284 to your computer and use it in GitHub Desktop.
React image uploader that allows to drag your image to position and clip it. Returns base64 string of the image via callback function.
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
/* | |
How to use: | |
<ImageInput | |
width={200} // required, width of the image component | |
height={200} // required, height of the image compoent | |
image='' // initial image value | |
onUpdate={(image) => { console.log(image) } } // do something the the image string in base64 | |
onUpdating={() => { console.log('component is currently being updated') }} | |
onUpdated={() => { console.log('component is currently has been updated') }} /> | |
*/ | |
import React, { Component } from 'react' | |
import PropTypes from 'prop-types' | |
const IMAGEURL = /(https?:\/\/.*\.(?:png|jpe?g|gif))/i | |
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' | |
import { faCheckCircle, faTimesCircle, faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons' | |
const checkFieldIsImage = (file) => { | |
if (file) { | |
if (typeof file !== 'string' && typeof file !== 'undefined') { | |
if (file.type.match(/image\/(png|jpeg|jpg)/)) { | |
return true | |
} | |
} else if (typeof file === 'string') { | |
return true | |
} | |
} | |
return false | |
} | |
class ImageUploader extends Component { | |
constructor(props) { | |
super(props) | |
const {width, height, image} = this.props | |
this.state = { | |
image: image, | |
file: null, | |
loading: false | |
} | |
this.canvasRef = null | |
this.canvasControl = React.createRef() | |
this.canvas = { | |
ctx: null, | |
width, | |
height, | |
offsetX: 0, | |
offsetY: 0 | |
} | |
this.mouse = { | |
x: 0, | |
y: 0, | |
sX: 0, | |
sY: 0 | |
} | |
this.image = { | |
source: null, | |
x: 0, | |
y: 0, | |
right: 0, | |
bottom: 0, | |
width: 0, | |
height: 0 | |
} | |
this.isDragging = false | |
this.inputRef = React.createRef() | |
this.file = null | |
} | |
shouldComponentUpdate(nextProps) { | |
if(nextProps.image !== this.props.image) { | |
this.setState({image: nextProps.image}) | |
} | |
return true | |
} | |
hitImage = (x, y) => { | |
return (x > this.image.x && x < this.image.x + this.image.width && | |
y > this.image.y && y < this.image.y + this.image.height) | |
} | |
handleMouseDown = (e) => { | |
this.mouse.sX = parseInt(e.clientX - this.canvas.offsetX) | |
this.mouse.sY = parseInt(e.clientY - this.canvas.offsetY) | |
this.isDragging = this.hitImage(this.mouse.sX, this.mouse.sY) | |
} | |
handleMouseUp = (e) => { | |
this.canvasControl.current.classList.remove('hide') | |
this.isDragging = false | |
} | |
handleMouseOut = (e) => { | |
this.canvasControl.current.classList.remove('hide') | |
this.isDragging = false | |
} | |
handleMouseMove = (e) => { | |
if (this.isDragging) { | |
this.canvasControl.current.classList.add('hide', 'dragged') | |
this.mouse.x = parseInt(e.clientX - this.canvas.offsetX) | |
this.mouse.y = parseInt(e.clientY - this.canvas.offsetY) | |
this.image.x += this.mouse.x - this.mouse.sX | |
this.image.y += this.mouse.y - this.mouse.sY | |
this.image.right = this.image.x + this.image.width | |
this.image.bottom = this.image.y + this.image.height | |
this.mouse.sX = this.mouse.x | |
this.mouse.sY = this.mouse.y | |
if (this.image.x > 0) { this.image.x = 0 } | |
if (this.image.y > 0) { this.image.y = 0 } | |
if (this.image.right < this.canvas.width) { this.image.x = this.canvas.width - this.image.width } | |
if (this.image.bottom < this.canvas.height) { this.image.y = this.canvas.height - this.image.height } | |
this.drawImage() | |
} | |
} | |
drawImage = () => { | |
this.canvas.ctx.fillStyle = '#adadad' | |
this.canvas.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height) | |
this.canvas.ctx.drawImage(this.image.source, | |
0, 0, this.image.source.width, this.image.source.height, | |
this.image.x, this.image.y, this.image.width, this.image.height | |
) | |
} | |
renderImageToCanvas = () => { | |
const { file } = this.state | |
const canvas = this.canvasRef | |
this.canvas.offsetX = canvas.parentElement.getBoundingClientRect().left | |
this.canvas.offsetY = canvas.parentElement.getBoundingClientRect().top | |
this.canvas.ctx = canvas.getContext('2d') | |
const img = new Image() /* global Image */ | |
// fixes "Tainted Canvas/CORS" issue | |
img.setAttribute('crossOrigin', 'anonymous') | |
img.onload = () => { | |
const hRatio = this.canvas.width / img.width | |
const vRatio = this.canvas.height / img.height | |
const ratio = Math.max(hRatio, vRatio) | |
this.image.width = img.width * ratio | |
this.image.height = img.height * ratio | |
this.image.x = (this.canvas.width - this.image.width) / 2 | |
this.image.y = (this.canvas.height - this.image.height) / 2 | |
this.image.right = this.image.x + this.image.width | |
this.image.bottom = this.image.y + this.image.height | |
this.drawImage() | |
this.setState({ loading: false }) | |
} | |
if (typeof file === 'string') { | |
img.src = file | |
} else { | |
img.src = URL.createObjectURL(file) | |
} | |
this.image.source = img | |
this.props.onUpdating && this.props.onUpdating() | |
} | |
imageAccept = (e) => { | |
e.preventDefault() | |
const imageData = this.canvasRef.toDataURL() | |
this.setState({ image: imageData, file: null }) | |
this.props.onUpdated && this.props.onUpdated() | |
this.props.onUpdate && this.props.onUpdate(imageData) | |
} | |
imageCancel = (e) => { | |
e.preventDefault() | |
this.props.onUpdated && this.props.onUpdated() | |
this.setState({ file: null }) | |
} | |
onDrop = (e) => { | |
e.preventDefault() | |
if (e.dataTransfer.items) { | |
// check if the dragged item is a file | |
if (e.dataTransfer.items[0].kind === 'file') { | |
const file = e.dataTransfer.items[0].getAsFile() | |
if (checkFieldIsImage(file)) { // check if the file is an image | |
this.setState({ file }) | |
} | |
} else if (e.dataTransfer.items[0].kind === 'string') { | |
e.dataTransfer.items[0].getAsString((str) => { | |
// verify if string url is a supported image | |
if (str !== '' && str.match(IMAGEURL)) { | |
this.setState({ file: str, loading: true }) | |
} | |
}) | |
} | |
} | |
} | |
onDragOver = (e) => { | |
e.preventDefault() | |
} | |
handleChange = (e) => { | |
e.preventDefault() | |
this.setState({ file: this.inputRef.current.files[0] }) | |
} | |
getCanvasRef = (node) => { | |
this.canvasRef = node | |
if (node) { | |
this.renderImageToCanvas() | |
} | |
} | |
render() { | |
const { image, file, loading } = this.state | |
if (checkFieldIsImage(file)) { | |
let Instructions = () => ( | |
<React.Fragment> | |
<div className="canvas-controls" ref={this.canvasControl}> | |
<a className="canvas-control-buttons accept" title="Accept" href="javascript:;" onClick={this.imageAccept}> | |
<FontAwesomeIcon icon={faCheckCircle} /> | |
</a> | |
<a className="canvas-control-buttons cancel" title="Reject" href="javascript:;" onClick={this.imageCancel}> | |
<FontAwesomeIcon icon={faTimesCircle} /> | |
</a> | |
</div> | |
</React.Fragment> | |
) | |
if (loading) { | |
Instructions = () => (<div className="canvas-instructions">Loading Image...</div>) | |
} | |
return ( | |
<div | |
className="canvas-container" | |
style={{width: `${this.canvas.width}px`, height: `${this.canvas.height}px`}} | |
onDrop={this.onDrop} | |
onDragOver={this.onDragOver} | |
> | |
<Instructions /> | |
<canvas | |
className="image-canvas" | |
ref={this.getCanvasRef} | |
width={this.canvas.width} | |
height={this.canvas.height} | |
onMouseUp={this.handleMouseUp} | |
onMouseDown={this.handleMouseDown} | |
onMouseOut={this.handleMouseOut} | |
onMouseMove={this.handleMouseMove} | |
/> | |
</div> | |
) | |
} | |
const UploadButton = () => image ? <button type="button" className="btn-update-img change"><FontAwesomeIcon icon={faCloudUploadAlt} /> Change</button> : <button type="button" className="btn-update-img"><FontAwesomeIcon icon={faCloudUploadAlt} /> Upload</button> | |
return ( | |
<div className="image-dropzone" style={{width: `${this.canvas.width}px`, height: `${this.canvas.height}px`}} onDrop={this.onDrop} onDragOver={this.onDragOver}> | |
<div className={`file-input`} onClick={() => this.inputRef.current.click()}> | |
<input | |
hidden | |
type='file' | |
ref={this.inputRef} | |
name="lead-image" | |
accept="image/jpeg, image/jpg, image/png" | |
onChange={this.handleChange} | |
/> | |
<div className={`img-input-component`}> | |
<img | |
className="img-container" | |
src={image} /> | |
<UploadButton /> | |
</div> | |
</div> | |
</div> | |
) | |
} | |
} | |
ImageUploader.propTypes = { | |
image: PropTypes.string, | |
width: PropTypes.number.isRequired, | |
height: PropTypes.number.isRequired, | |
onUpdate: PropTypes.func, | |
onUpdated: PropTypes.func | |
} | |
export default ImageUploader |
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
.image-dropzone { | |
position: relative; | |
border: 1px solid #ddd; | |
.file-input { | |
position: absolute; | |
width: 100%; | |
height: 100%; | |
} | |
.file-input .img-input-component { | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
height: 100%; | |
} | |
.file-input .img-input-component .btn-update-img { | |
width: 100%; | |
height: 100%; | |
z-index: 1; | |
border: none; | |
background-color: #fff; | |
font-size: 1.2em; | |
&.change { | |
background-color: rgba(#fff, 0.5); | |
opacity: 0; | |
transition: opacity 0.5s; | |
&:hover { | |
opacity: 1; | |
} | |
} | |
&:active, | |
&:focus { | |
outline: none; | |
box-shadow: none; | |
} | |
} | |
.file-input .img-input-component img { | |
position: absolute; | |
width: 100%; | |
} | |
} | |
.canvas-container { | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
.canvas-controls { | |
position: absolute; | |
font-size: 34px; | |
line-height: 1; | |
transition: opacity 0.5s, visibility 0.5s; | |
&.dragged .canvas-control-buttons{ | |
box-shadow: 0px 0px 8px 0px rgba(#000, 0.30); | |
} | |
&.hide { | |
opacity: 0; | |
visibility: hidden; | |
} | |
} | |
.canvas-control-buttons { | |
background-color: #fff; | |
border-radius: 50%; | |
overflow: hidden; | |
height: 34px; | |
width: 34px; | |
display: inline-block; | |
margin: 4px 8px; | |
&.accept { | |
color: #28a745; | |
} | |
&.cancel { | |
color: #6c757d; | |
} | |
.icons { | |
display: block; | |
margin-left: -2px; | |
margin-top: -8px; | |
} | |
} | |
.image-canvas { | |
cursor: move; | |
border: 1px solid #ddd; | |
} | |
} | |
.entry-media { | |
margin-bottom: 8px; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment