1+ import accepts from 'attr-accept' ;
2+ import React , { Component , PropTypes } from 'react' ;
3+
4+ export default class DropZone extends Component {
5+ static propTypes = {
6+ // Overriding drop behavior
7+ onDrop : PropTypes . func ,
8+ onDropAccepted : PropTypes . func ,
9+ onDropRejected : PropTypes . func ,
10+
11+ // Overriding drag behavior
12+ onDragStart : PropTypes . func ,
13+ onDragEnter : PropTypes . func ,
14+ onDragLeave : PropTypes . func ,
15+
16+ children : PropTypes . node , // Contents of the dropzone
17+ style : PropTypes . object , // CSS styles to apply
18+ activeStyle : PropTypes . object , // CSS styles to apply when drop will be accepted
19+ rejectStyle : PropTypes . object , // CSS styles to apply when drop will be rejected
20+ className : PropTypes . string , // Optional className
21+ activeClassName : PropTypes . string , // className for accepted state
22+ rejectClassName : PropTypes . string , // className for rejected state
23+
24+ disablePreview : PropTypes . bool , // Enable/disable preview generation
25+ disableClick : PropTypes . bool , // Disallow clicking on the dropzone container to open file dialog
26+
27+ inputProps : PropTypes . object , // Pass additional attributes to the <input type="file"/> tag
28+ multiple : PropTypes . bool , // Allow dropping multiple files
29+ accept : PropTypes . string , // Allow specific types of files. See https://github.com/okonet/attr-accept for more information
30+ name : PropTypes . string // name attribute for the input tag
31+ } ;
32+
33+
34+ static defaultProps = {
35+ disablePreview : false ,
36+ disableClick : false ,
37+ multiple : true
38+ } ;
39+
40+ state = {
41+ isDragActive : false
42+ } ;
43+
44+ componentDidMount ( ) {
45+ this . enterCounter = 0 ;
46+ }
47+
48+ onDragStart = ( e ) => {
49+ if ( this . props . onDragStart ) {
50+ this . props . onDragStart . call ( this , e ) ;
51+ }
52+ } ;
53+
54+ onDragEnter = ( e ) => {
55+ const { onDragEnter} = this . props ;
56+
57+ e . preventDefault ( ) ;
58+
59+ // Count the dropzone and any children that are entered.
60+ ++ this . enterCounter ;
61+
62+ // This is tricky. During the drag even the dataTransfer.files is null
63+ // But Chrome implements some drag store, which is accesible via dataTransfer.items
64+ const dataTransferItems = e . dataTransfer && e . dataTransfer . items ? e . dataTransfer . items : [ ] ;
65+
66+ // Now we need to convert the DataTransferList to Array
67+ const allFilesAccepted = this . allFilesAccepted ( Array . prototype . slice . call ( dataTransferItems ) ) ;
68+
69+ this . setState ( {
70+ isDragActive : allFilesAccepted ,
71+ isDragReject : ! allFilesAccepted
72+ } ) ;
73+
74+ if ( onDragEnter ) {
75+ onDragEnter . call ( this , e ) ;
76+ }
77+ } ;
78+
79+ onDragOver = ( e ) => {
80+ e . preventDefault ( ) ;
81+ e . stopPropagation ( ) ;
82+ return false ;
83+ } ;
84+
85+ onDragLeave = ( e ) => {
86+ const { onDragLeave} = this . props ;
87+
88+ e . preventDefault ( ) ;
89+
90+ // Only deactivate once the dropzone and all children was left.
91+ if ( -- this . enterCounter > 0 ) {
92+ return ;
93+ }
94+
95+ this . setState ( {
96+ isDragActive : false ,
97+ isDragReject : false
98+ } ) ;
99+
100+ if ( onDragLeave ) {
101+ onDragLeave . call ( this , e ) ;
102+ }
103+ } ;
104+
105+ onDrop = ( e ) => {
106+ const { onDrop, onDropAccepted, onDropRejected} = this . props ;
107+
108+ e . preventDefault ( ) ;
109+
110+ // Reset the counter along with the drag on a drop.
111+ this . enterCounter = 0 ;
112+
113+ this . setState ( {
114+ isDragActive : false ,
115+ isDragReject : false
116+ } ) ;
117+
118+ const droppedFiles = e . dataTransfer ? e . dataTransfer . files : e . target . files ;
119+ const max = this . props . multiple ? droppedFiles . length : Math . min ( droppedFiles . length , 1 ) ;
120+ const files = [ ] ;
121+
122+ for ( let i = 0 ; i < max ; i ++ ) {
123+ const file = droppedFiles [ i ] ;
124+ // We might want to disable the preview creation to support big files
125+ if ( ! this . props . disablePreview ) {
126+ file . preview = window . URL . createObjectURL ( file ) ;
127+ }
128+
129+ files . push ( file ) ;
130+ }
131+
132+ if ( this . allFilesAccepted ( files ) ) {
133+ if ( onDrop ) {
134+ onDrop . call ( this , files , e ) ;
135+ }
136+
137+ if ( onDropAccepted ) {
138+ onDropAccepted . call ( this , files , e ) ;
139+ }
140+ } else {
141+ if ( onDropRejected ) {
142+ onDropRejected . call ( this , files , e ) ;
143+ }
144+ }
145+ } ;
146+
147+ onClick = ( ) => {
148+ if ( ! this . props . disableClick ) {
149+ this . open ( ) ;
150+ }
151+ } ;
152+
153+ allFilesAccepted ( files ) {
154+ return files . every ( file => accepts ( file , this . props . accept ) ) ;
155+ }
156+
157+ open ( ) {
158+ this . fileInputEl . value = null ;
159+ this . fileInputEl . click ( ) ;
160+ }
161+
162+ render ( ) {
163+ const {
164+ accept,
165+ activeClassName,
166+ inputProps,
167+ multiple,
168+ name,
169+ rejectClassName,
170+ ...rest
171+ } = this . props ;
172+
173+ let {
174+ activeStyle,
175+ className,
176+ rejectStyle,
177+ style,
178+ ...props // eslint-disable-line prefer-const
179+ } = rest ;
180+
181+ const { isDragActive, isDragReject} = this . state ;
182+
183+ className = className || '' ;
184+
185+ if ( isDragActive && activeClassName ) {
186+ className += ' ' + activeClassName ;
187+ }
188+ if ( isDragReject && rejectClassName ) {
189+ className += ' ' + rejectClassName ;
190+ }
191+
192+ if ( ! className && ! style && ! activeStyle && ! rejectStyle ) {
193+ style = {
194+ backgroundColor : "#fafafa" ,
195+ width : 'auto' ,
196+ borderWidth : 1 ,
197+ borderColor : '#757575' ,
198+ borderStyle : 'dashed' ,
199+ borderRadius : 2
200+ } ;
201+ activeStyle = {
202+ borderStyle : 'solid' ,
203+ backgroundColor : '#fafafa'
204+ } ;
205+ rejectStyle = {
206+ borderStyle : 'solid' ,
207+ backgroundColor : '#fafafa'
208+ } ;
209+ }
210+
211+ let appliedStyle ;
212+ if ( activeStyle && isDragActive ) {
213+ appliedStyle = {
214+ ...style ,
215+ ...activeStyle
216+ } ;
217+ } else if ( rejectStyle && isDragReject ) {
218+ appliedStyle = {
219+ ...style ,
220+ ...rejectStyle
221+ } ;
222+ } else {
223+ appliedStyle = {
224+ ...style
225+ } ;
226+ }
227+
228+ const inputAttributes = {
229+ accept,
230+ type : 'file' ,
231+ style : { display : 'none' } ,
232+ multiple : multiple ,
233+ ref : el => this . fileInputEl = el , // eslint-disable-line
234+ onChange : this . onDrop
235+ } ;
236+
237+ if ( name && name . length ) {
238+ inputAttributes . name = name ;
239+ }
240+
241+ // Remove custom properties before passing them to the wrapper div element
242+ const customProps = [ 'disablePreview' , 'disableClick' , 'onDropAccepted' , 'onDropRejected' ] ;
243+ const divProps = { ...props } ;
244+ customProps . forEach ( prop => delete divProps [ prop ] ) ;
245+
246+ return (
247+ < div
248+ className = { className }
249+ style = { appliedStyle }
250+ { ...divProps /* expand user provided props first so event handlers are never overridden */ }
251+ onClick = { this . onClick }
252+ onDragStart = { this . onDragStart }
253+ onDragEnter = { this . onDragEnter }
254+ onDragOver = { this . onDragOver }
255+ onDragLeave = { this . onDragLeave }
256+ onDrop = { this . onDrop }
257+ >
258+ { this . props . children }
259+ < input
260+ { ...inputProps /* expand user provided inputProps first so inputAttributes override them */ }
261+ { ...inputAttributes }
262+ multiple />
263+ </ div >
264+ ) ;
265+ }
266+ }
0 commit comments