src/js/upload/index.js
import React from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import accept from 'attr-accept'
import Button from '../button/'
export default class Upload extends React.Component {
static propTypes = {
accept: PropTypes.string,
disabled: PropTypes.bool,
onChange: PropTypes.func.isRequired,
buttonText: PropTypes.string,
textBefore: PropTypes.string,
textAfter: PropTypes.string
}
static defaultProps = {
accept: '*',
disabled: false,
buttonText: 'mdc-Select File',
textBefore: '',
textAfter: 'or drag file in this area'
}
state = {
isOverWindow: false,
isOverDropzone: false
}
// counter is an internal variable which is updated during drag enter and drag leave events.
// the problem is that the browser fires drag leave and drag enter events
// when dragging over a child element of window.
counter = 0
// prevent drag and drop of multiple files
// this only works in chrome since Firefox does not allow access to files while dragging
isInvalid (event) {
const edt = event.dataTransfer
// event.dataTransfer is empty during tests. no need to check every time in the following if ...
if (!edt) return false
// check for disabled
if (this.props.disabled) return true
// check firefox
if (edt.mozItemCount && edt.mozItemCount !== 1) {
return true
}
// check chrome
if (edt.items && edt.items.length !== 1) {
return true
}
// check mime types. only works in chrome. files is a FileList (no real array)
const unaccepted = edt.files && Array.prototype.slice.call(edt.files).every((file) =>
!accept(file, this.props.accept)
)
return unaccepted
}
onDragEnter = (event) => {
if (this.isInvalid(event)) return
// prevent event to allow drop
event.preventDefault()
this.setState({
isOverDropzone: true
})
}
onDragOver = (event) => {
if (this.isInvalid(event)) return
// If you want to allow a drop, you must prevent the default handling by cancelling the event
// https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Drag_operations#Specifying_Drop_Targets
event.preventDefault()
}
onDragLeave = (event) => {
event.preventDefault()
this.setState({
isOverDropzone: false
})
}
onDocumentDragEnter = (event) => {
// prevent drag & drop of multiple items
if (this.isInvalid(event)) return
++this.counter
this.setState({
isOverWindow: true
})
}
onDocumentDragLeave = (event) => {
event.preventDefault()
if (--this.counter > 0) {
return
}
// update state only when really leaving the document
// not when leaving document children
this.setState({
isOverWindow: false
})
}
onChange = (event) => {
event.preventDefault()
this.counter = 0
// event.target.files is from standard file upload dialog
// event.dataTransfer.files is from drap and drop api
let files = event.target.files || event.dataTransfer.files
this.setState({
isOverWindow: false,
isOverDropzone: false
})
// call callback to send files to parent
this.props.onChange(files)
}
onClick = (event) => {
this.fileInput.click()
}
componentDidMount () {
document.addEventListener('dragenter', this.onDocumentDragEnter)
document.addEventListener('dragleave', this.onDocumentDragLeave)
}
componentWillUnmount () {
document.removeEventListener('dragenter', this.onDocumentDragEnter)
document.removeEventListener('dragleave', this.onDocumentDragLeave)
}
render () {
const klass = classNames('Upload', {
'is-disabled': this.props.disabled,
'is-over-window': this.state.isOverWindow,
'is-over-dropzone': this.state.isOverDropzone
})
return (
<div
className={klass}
onDragEnter={this.onDragEnter}
onDragOver={this.onDragOver}
onDragLeave={this.onDragLeave}
onDrop={this.onChange}
>
<div className='Upload-textBefore'>
{this.props.textBefore}
</div>
<Button raised onClick={this.onClick} disabled={this.props.disabled}>
{this.props.buttonText}
</Button>
<div className='Upload-textAfter'>
{this.props.textAfter}
</div>
<input
accept={this.props.accept}
className='Upload-fileInput'
disabled={this.props.disabled}
type='file'
onChange={this.onChange}
ref={(el) => (this.fileInput = el)}
/>
</div>
)
}
}