modules-pack
Version:
JavaScript Modules for Modern Frontend & Backend Projects
227 lines (211 loc) • 8.18 kB
JavaScript
import classNames from 'classnames'
import { POPUP, POPUP_ALERT } from 'modules-pack/popup/constants'
import { connect, stateAction } from 'modules-pack/redux'
import { ROUTE_HOME, UPLOAD as U } from 'modules-pack/variables'
import React, { Fragment, PureComponent } from 'react'
import Dropzone from 'react-dropzone'
import { type, withTimer } from 'react-ui-pack'
import Icon from 'react-ui-pack/Icon'
import Loading from 'react-ui-pack/Loading'
import Row from 'react-ui-pack/Row'
import Text from 'react-ui-pack/Text'
import Tooltip from 'react-ui-pack/Tooltip'
import View from 'react-ui-pack/View'
import {
get,
hasListValue,
interpolateString as parseString,
isFunction,
log,
OPEN,
pluralize,
SET,
shortNumber,
SIZE_KB
} from 'utils-pack'
import { UPLOAD } from '../constants'
import select from '../selectors'
import { _ } from '../translations'
/**
* MAP STATE & ACTIONS TO PROPS ------------------------------------------------
* -----------------------------------------------------------------------------
*/
const mapStateToProps = (state) => ({
loading: select.loading(state)
})
const mapDispatchToProps = (dispatch) => ({
actions: {
upload: (files, fileType) => dispatch(stateAction(UPLOAD, SET, {files, fileType})),
popup: (item) => dispatch(stateAction(POPUP, OPEN, {
activePopup: POPUP_ALERT,
[POPUP_ALERT]: {
items: [item]
}
}))
}
})
/**
* VIEW TEMPLATE ---------------------------------------------------------------
* todo: fix Dropzone opening upload window twice (try fast upload)
* -----------------------------------------------------------------------------
*/
export default class Upload extends PureComponent {
static propTypes = {
// Upload file type, falls back to Route pathname
// If given, will render as embedded component, instead of Modal route
fileType: type.OneOf(type.String, type.Number),
/* Allowed file formats, example: ['jpg', 'png'] */
formats: type.ListOf(type.String),
/* Maximum File size in bytes */
maxSize: type.Number,
/* Callback(acceptedFiles, name) onDrop files */
onChange: type.Method,
/* Callback when close button is clicked (ex. history.goBack()) */
onClose: type.Method,
/* Callback when cancel upload or on drag leave */
onBlur: type.Method,
/* Callback when choose file or on drag enter */
onFocus: type.Method,
/* Callback(instance, ...componentWillMountProps) before component mounts */
onComponentWillMount: type.Method,
loading: type.Boolean,
disabled: type.Boolean, // whether to disable upload
readonly: type.Boolean, // whether to make upload viewable only
multiple: type.Boolean, // whether to allow multiple file uploads, true by default
hasHeader: type.Boolean, // whether to show title above the upload
showTypes: type.Boolean, // whether to show file types tooltip
round: type.Boolean, // whether to add `round` css class
label: type.String, // optional label to show in the title
labelOnHover: type.String, // optional label to show on Dropzone hover
children: type.Any,
}
static defaultProps = {
loading: false,
multiple: true,
showTypes: true,
}
state = {
active: false
}
get formats () {
const {formats} = this.props
return formats ? `.${formats.join(', .')}` : (U.BY_ROUTE[this.fileType] || {}).fileTypes
}
get maxSize () {
return this.props.maxSize || (U.BY_ROUTE[this.fileType] || {}).maxSize
}
get fileType () {
return this.props.fileType || this.uri.split(/\//).pop().toLowerCase()
}
get uri () {
return get(this.props, 'location.pathname', ROUTE_HOME)
}
onDragEnter = (...args) => this.setState({active: true}, () => this.props.onFocus && this.props.onFocus(...args))
onDragLeave = (...args) => this.setState({active: false}, () => this.props.onBlur && this.props.onBlur(...args))
handleUpload = (acceptedFiles, rejectedFiles) => {
log('acceptedFiles:', acceptedFiles)
log('rejectedFiles:', rejectedFiles)
const {actions, onChange, name} = this.props
if (hasListValue(acceptedFiles)) {
const maxSize = this.maxSize
for (const file of acceptedFiles) {
if (file.size > maxSize) return actions.popup({
title: _.MAXIMUM_FILE_SIZE_EXCEEDED,
content: <Row className="center wrap">
<Text className="bold margin-h-smaller">{file.name}</Text>
<Text>{_.MUST_BE_UNDER}</Text>
<Text className="bold margin-h-smaller">{shortNumber(maxSize, 3, SIZE_KB)}B</Text>
</Row>,
closeLabel: _.OK
})
}
actions.upload(acceptedFiles, this.fileType)
isFunction(onChange) && onChange(acceptedFiles, name)
} else {
actions.popup({
title: _.FILE_UPLOAD_FAILED,
content: <View className='center wrap'>
<Text>{_.UPLOAD}</Text>
<Text className="p bold">{this.formats}</Text>
<Text>{_.FILES_ONLY}</Text>
</View>,
closeLabel: _.OK
})
}
}
handleKeyPress = event => {
if (event.key === 'Enter') this.dropzone.open()
}
renderClose = (handleClose) => (
<View className="app__view--close" onClick={handleClose}>
<Text className="app__view--close__icon">{'✕'}</Text>
<Tooltip top>{_.CLOSE}</Tooltip>
</View>
)
UNSAFE_componentWillMount (...args) {
this.props.onComponentWillMount && this.props.onComponentWillMount(this, ...args)
// if (this.props.fileType == null && !window.prevLocation) { // if this route is accessed directly in browser
// history.push(ROUTE_HOME) // go to homepage first,
// this.setTimeout(() => { // then open as Modal
// openModal(this.uri, {className: 'fill--three-quarter'})
// }, 100)
// }
}
render () {
const {
loading, children, multiple, disabled, readonly, onBlur, name, labelOnHover, onClose,
className, classWrap, hasHeader, round, showTypes, title,
} = this.props
const label = this.props.label || this.fileType || _.FILE
const {active} = this.state
const formats = this.formats
return (
<View className={classNames('app__upload', classWrap, {round})}>
{onClose && this.renderClose(onClose)}
{hasHeader && <h2>{parseString(_.UPLOAD_file, {file: label})}</h2>}
<Dropzone
// @note: When tabbing to dropzone with keyboard, input[type="file"] also gets event -> causing open twice.
// => input is hidden by dropzone because it has ugly "Choose File" button
name={name}
title={title}
inputProps={inputProps}
tabIndex={disabled ? -1 : 0}
className={classNames('upload__dropzone', className, {active, round, disabled, readonly})}
ref={(node) => { this.dropzone = node }}
onDragEnter={this.onDragEnter}
onDragLeave={this.onDragLeave}
onDrop={this.handleUpload}
onFileDialogCancel={onBlur}
onKeyPress={this.handleKeyPress}
accept={formats}
multiple={multiple}
disabled={disabled || readonly}
>
{children || <Fragment>
<Icon name="image" className="text largest no-margin"/>
<Text className='p margin-top-smallest'>
{_.SELECT_OR_DROP}<br/>
{pluralize(label, multiple ? 2 : 1)}
</Text>
</Fragment>
}
{showTypes &&
<View className="dropzone__hover position-fill align-center appear-on-hover">
<View className="padding text-outline">
<View className="dropzone__hover__bg position-fill bg-neutral radius-large"/>
<Text className="margin-bottom-smaller">
{labelOnHover || parseString(_.UPLOAD_file_FILE, {file: label})}
</Text>
<Text className="bold p">{formats.replace(/\./g, '')}</Text>
</View>
</View>
}
</Dropzone>
<Loading loading={loading}/>
</View>
)
}
}
const inputProps = {tabIndex: -1}