Skip to content

Instantly share code, notes, and snippets.

@ikawka
Last active May 14, 2019 06:19
Show Gist options
  • Save ikawka/1234141fa431da6b833434b13a09c284 to your computer and use it in GitHub Desktop.
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.
/*
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
.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