siam-ui-utils 2.2.18 → 2.2.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,816 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+
4
+ import LayoutDefault from './Layout'
5
+ import InputDefault from './Input'
6
+ import PreviewDefault from './Preview'
7
+ import SubmitButtonDefault from './SubmitButton'
8
+ import {
9
+ formatBytes,
10
+ formatDuration,
11
+ accepts,
12
+ resolveValue,
13
+ mergeStyles,
14
+ defaultClassNames,
15
+ getFilesFromEvent as defaultGetFilesFromEvent,
16
+ } from './utils'
17
+
18
+ export type StatusValue =
19
+ | 'rejected_file_type'
20
+ | 'rejected_max_files'
21
+ | 'preparing'
22
+ | 'error_file_size'
23
+ | 'error_validation'
24
+ | 'ready'
25
+ | 'started'
26
+ | 'getting_upload_params'
27
+ | 'error_upload_params'
28
+ | 'uploading'
29
+ | 'exception_upload'
30
+ | 'aborted'
31
+ | 'restarted'
32
+ | 'removed'
33
+ | 'error_upload'
34
+ | 'headers_received'
35
+ | 'done'
36
+
37
+ export type MethodValue =
38
+ | 'delete'
39
+ | 'get'
40
+ | 'head'
41
+ | 'options'
42
+ | 'patch'
43
+ | 'post'
44
+ | 'put'
45
+ | 'DELETE'
46
+ | 'GET'
47
+ | 'HEAD'
48
+ | 'OPTIONS'
49
+ | 'PATCH'
50
+ | 'POST'
51
+ | 'PUT'
52
+
53
+ export interface IMeta {
54
+ id: string
55
+ status: StatusValue
56
+ type: string // MIME type, example: `image/*`
57
+ name: string
58
+ uploadedDate: string // ISO string
59
+ percent: number
60
+ size: number // bytes
61
+ lastModifiedDate: string // ISO string
62
+ previewUrl?: string // from URL.createObjectURL
63
+ duration?: number // seconds
64
+ width?: number
65
+ height?: number
66
+ videoWidth?: number
67
+ videoHeight?: number
68
+ validationError?: any
69
+ }
70
+
71
+ export interface IFileWithMeta {
72
+ file: File
73
+ meta: IMeta
74
+ cancel: () => void
75
+ restart: () => void
76
+ remove: () => void
77
+ xhr?: XMLHttpRequest
78
+ }
79
+
80
+ export interface IExtra {
81
+ active: boolean
82
+ reject: boolean
83
+ dragged: DataTransferItem[]
84
+ accept: string
85
+ multiple: boolean
86
+ minSizeBytes: number
87
+ maxSizeBytes: number
88
+ maxFiles: number
89
+ }
90
+
91
+ export interface IUploadParams {
92
+ url: string
93
+ method?: MethodValue
94
+ body?: string | FormData | ArrayBuffer | Blob | File | URLSearchParams
95
+ fields?: { [name: string]: string | Blob }
96
+ headers?: { [name: string]: string }
97
+ meta?: { [name: string]: any }
98
+ }
99
+
100
+ export type CustomizationFunction<T> = (allFiles: IFileWithMeta[], extra: IExtra) => T
101
+
102
+ export interface IStyleCustomization<T> {
103
+ dropzone?: T | CustomizationFunction<T>
104
+ dropzoneActive?: T | CustomizationFunction<T>
105
+ dropzoneReject?: T | CustomizationFunction<T>
106
+ dropzoneDisabled?: T | CustomizationFunction<T>
107
+ input?: T | CustomizationFunction<T>
108
+ inputLabel?: T | CustomizationFunction<T>
109
+ inputLabelWithFiles?: T | CustomizationFunction<T>
110
+ preview?: T | CustomizationFunction<T>
111
+ previewImage?: T | CustomizationFunction<T>
112
+ submitButtonContainer?: T | CustomizationFunction<T>
113
+ submitButton?: T | CustomizationFunction<T>
114
+ }
115
+
116
+ export interface IExtraLayout extends IExtra {
117
+ onFiles(files: File[]): void
118
+ onCancelFile(file: IFileWithMeta): void
119
+ onRemoveFile(file: IFileWithMeta): void
120
+ onRestartFile(file: IFileWithMeta): void
121
+ }
122
+
123
+ export interface ILayoutProps {
124
+ files: IFileWithMeta[]
125
+ extra: IExtraLayout
126
+ input: React.ReactNode
127
+ previews: React.ReactNode[] | null
128
+ submitButton: React.ReactNode
129
+ dropzoneProps: {
130
+ ref: React.RefObject<HTMLDivElement>
131
+ className: string
132
+ style?: React.CSSProperties
133
+ onDragEnter(event: React.DragEvent<HTMLElement>): void
134
+ onDragOver(event: React.DragEvent<HTMLElement>): void
135
+ onDragLeave(event: React.DragEvent<HTMLElement>): void
136
+ onDrop(event: React.DragEvent<HTMLElement>): void
137
+ }
138
+ }
139
+
140
+ interface ICommonProps {
141
+ files: IFileWithMeta[]
142
+ extra: IExtra
143
+ }
144
+
145
+ export interface IPreviewProps extends ICommonProps {
146
+ meta: IMeta
147
+ className?: string
148
+ imageClassName?: string
149
+ style?: React.CSSProperties
150
+ imageStyle?: React.CSSProperties
151
+ fileWithMeta: IFileWithMeta
152
+ isUpload: boolean
153
+ canCancel: boolean
154
+ canRemove: boolean
155
+ canRestart: boolean
156
+ }
157
+
158
+ export interface IInputProps extends ICommonProps {
159
+ className?: string
160
+ labelClassName?: string
161
+ labelWithFilesClassName?: string
162
+ style?: React.CSSProperties
163
+ labelStyle?: React.CSSProperties
164
+ labelWithFilesStyle?: React.CSSProperties
165
+ getFilesFromEvent: (event: React.ChangeEvent<HTMLInputElement>) => Promise<File[]>
166
+ accept: string
167
+ multiple: boolean
168
+ disabled: boolean
169
+ content?: React.ReactNode
170
+ withFilesContent?: React.ReactNode
171
+ onFiles: (files: File[]) => void
172
+ }
173
+
174
+ export interface ISubmitButtonProps extends ICommonProps {
175
+ className?: string
176
+ buttonClassName?: string
177
+ style?: React.CSSProperties
178
+ buttonStyle?: React.CSSProperties
179
+ disabled: boolean
180
+ content?: React.ReactNode
181
+ onSubmit: (files: IFileWithMeta[]) => void
182
+ }
183
+
184
+ type ReactComponent<Props> = (props: Props) => React.ReactNode | React.Component<Props>
185
+
186
+ export interface IDropzoneProps {
187
+ onChangeStatus?(
188
+ file: IFileWithMeta,
189
+ status: StatusValue,
190
+ allFiles: IFileWithMeta[],
191
+ ): { meta: { [name: string]: any } } | void
192
+ getUploadParams?(file: IFileWithMeta): IUploadParams | Promise<IUploadParams>
193
+ onSubmit?(successFiles: IFileWithMeta[], allFiles: IFileWithMeta[]): void
194
+
195
+ getFilesFromEvent?: (
196
+ event: React.DragEvent<HTMLElement> | React.ChangeEvent<HTMLInputElement>,
197
+ ) => Promise<File[]> | File[]
198
+ getDataTransferItemsFromEvent?: (
199
+ event: React.DragEvent<HTMLElement>,
200
+ ) => Promise<DataTransferItem[]> | DataTransferItem[]
201
+
202
+ accept: string
203
+ multiple: boolean
204
+ minSizeBytes: number
205
+ maxSizeBytes: number
206
+ maxFiles: number
207
+
208
+ validate?(file: IFileWithMeta): any // usually a string, but can be anything
209
+
210
+ autoUpload: boolean
211
+ timeout?: number
212
+
213
+ initialFiles?: File[]
214
+
215
+ /* component customization */
216
+ disabled: boolean | CustomizationFunction<boolean>
217
+
218
+ canCancel: boolean | CustomizationFunction<boolean>
219
+ canRemove: boolean | CustomizationFunction<boolean>
220
+ canRestart: boolean | CustomizationFunction<boolean>
221
+
222
+ inputContent: React.ReactNode | CustomizationFunction<React.ReactNode>
223
+ inputWithFilesContent: React.ReactNode | CustomizationFunction<React.ReactNode>
224
+
225
+ submitButtonDisabled: boolean | CustomizationFunction<boolean>
226
+ submitButtonContent: React.ReactNode | CustomizationFunction<React.ReactNode>
227
+
228
+ classNames: IStyleCustomization<string>
229
+ styles: IStyleCustomization<React.CSSProperties>
230
+ addClassNames: IStyleCustomization<string>
231
+
232
+ /* component injection */
233
+ LayoutComponent?: ReactComponent<ILayoutProps>
234
+ PreviewComponent?: ReactComponent<IPreviewProps>
235
+ InputComponent?: ReactComponent<IInputProps>
236
+ SubmitButtonComponent?: ReactComponent<ISubmitButtonProps>
237
+ }
238
+
239
+ class Dropzone extends React.Component<IDropzoneProps, { active: boolean; dragged: (File | DataTransferItem)[] }> {
240
+ static defaultProps: IDropzoneProps
241
+ protected files: IFileWithMeta[]
242
+ protected mounted: boolean
243
+ protected dropzone: React.RefObject<HTMLDivElement>
244
+ protected dragTimeoutId?: number
245
+
246
+ constructor(props: IDropzoneProps) {
247
+ super(props)
248
+ this.state = {
249
+ active: false,
250
+ dragged: [],
251
+ }
252
+ this.files = []
253
+ this.mounted = true
254
+ this.dropzone = React.createRef()
255
+ }
256
+
257
+ componentDidMount() {
258
+ if (this.props.initialFiles) this.handleFiles(this.props.initialFiles)
259
+ }
260
+
261
+ componentDidUpdate(prevProps: IDropzoneProps) {
262
+ const { initialFiles } = this.props
263
+ if (prevProps.initialFiles !== initialFiles && initialFiles) this.handleFiles(initialFiles)
264
+ }
265
+
266
+ componentWillUnmount() {
267
+ this.mounted = false
268
+ for (const fileWithMeta of this.files) this.handleCancel(fileWithMeta)
269
+ }
270
+
271
+ forceUpdate = () => {
272
+ if (this.mounted) super.forceUpdate()
273
+ }
274
+
275
+ getFilesFromEvent = () => {
276
+ return this.props.getFilesFromEvent || defaultGetFilesFromEvent
277
+ }
278
+
279
+ getDataTransferItemsFromEvent = () => {
280
+ return this.props.getDataTransferItemsFromEvent || defaultGetFilesFromEvent
281
+ }
282
+
283
+ handleDragEnter = async (e: React.DragEvent<HTMLElement>) => {
284
+ e.preventDefault()
285
+ e.stopPropagation()
286
+ const dragged = (await this.getDataTransferItemsFromEvent()(e)) as DataTransferItem[]
287
+ this.setState({ active: true, dragged })
288
+ }
289
+
290
+ handleDragOver = async (e: React.DragEvent<HTMLElement>) => {
291
+ e.preventDefault()
292
+ e.stopPropagation()
293
+ clearTimeout(this.dragTimeoutId)
294
+ const dragged = await this.getDataTransferItemsFromEvent()(e)
295
+ this.setState({ active: true, dragged })
296
+ }
297
+
298
+ handleDragLeave = (e: React.DragEvent<HTMLElement>) => {
299
+ e.preventDefault()
300
+ e.stopPropagation()
301
+ // prevents repeated toggling of `active` state when file is dragged over children of uploader
302
+ // see: https://www.smashingmagazine.com/2018/01/drag-drop-file-uploader-vanilla-js/
303
+ this.dragTimeoutId = window.setTimeout(() => this.setState({ active: false, dragged: [] }), 150)
304
+ }
305
+
306
+ handleDrop = async (e: React.DragEvent<HTMLElement>) => {
307
+ e.preventDefault()
308
+ e.stopPropagation()
309
+ this.setState({ active: false, dragged: [] })
310
+ const files = (await this.getFilesFromEvent()(e)) as File[]
311
+ this.handleFiles(files)
312
+ }
313
+
314
+ handleDropDisabled = (e: React.DragEvent<HTMLElement>) => {
315
+ e.preventDefault()
316
+ e.stopPropagation()
317
+ this.setState({ active: false, dragged: [] })
318
+ }
319
+
320
+ handleChangeStatus = (fileWithMeta: IFileWithMeta) => {
321
+ if (!this.props.onChangeStatus) return
322
+ const { meta = {} } = this.props.onChangeStatus(fileWithMeta, fileWithMeta.meta.status, this.files) || {}
323
+ if (meta) {
324
+ delete meta.status
325
+ fileWithMeta.meta = { ...fileWithMeta.meta, ...meta }
326
+ this.forceUpdate()
327
+ }
328
+ }
329
+
330
+ handleSubmit = (files: IFileWithMeta[]) => {
331
+ if (this.props.onSubmit) this.props.onSubmit(files, [...this.files])
332
+ }
333
+
334
+ handleCancel = (fileWithMeta: IFileWithMeta) => {
335
+ if (fileWithMeta.meta.status !== 'uploading') return
336
+ fileWithMeta.meta.status = 'aborted'
337
+ if (fileWithMeta.xhr) fileWithMeta.xhr.abort()
338
+ this.handleChangeStatus(fileWithMeta)
339
+ this.forceUpdate()
340
+ }
341
+
342
+ handleRemove = (fileWithMeta: IFileWithMeta) => {
343
+ const index = this.files.findIndex(f => f === fileWithMeta)
344
+ if (index !== -1) {
345
+ URL.revokeObjectURL(fileWithMeta.meta.previewUrl || '')
346
+ fileWithMeta.meta.status = 'removed'
347
+ this.handleChangeStatus(fileWithMeta)
348
+ this.files.splice(index, 1)
349
+ this.forceUpdate()
350
+ }
351
+ }
352
+
353
+ handleRestart = (fileWithMeta: IFileWithMeta) => {
354
+ if (!this.props.getUploadParams) return
355
+
356
+ if (fileWithMeta.meta.status === 'ready') fileWithMeta.meta.status = 'started'
357
+ else fileWithMeta.meta.status = 'restarted'
358
+ this.handleChangeStatus(fileWithMeta)
359
+
360
+ fileWithMeta.meta.status = 'getting_upload_params'
361
+ fileWithMeta.meta.percent = 0
362
+ this.handleChangeStatus(fileWithMeta)
363
+ this.forceUpdate()
364
+ this.uploadFile(fileWithMeta)
365
+ }
366
+
367
+ // expects an array of File objects
368
+ handleFiles = (files: File[]) => {
369
+ files.forEach((f, i) => this.handleFile(f, `${new Date().getTime()}-${i}`))
370
+ const { current } = this.dropzone
371
+ if (current) setTimeout(() => current.scroll({ top: current.scrollHeight, behavior: 'smooth' }), 150)
372
+ }
373
+
374
+ handleFile = async (file: File, id: string) => {
375
+ const { name, size, type, lastModified } = file
376
+ const { minSizeBytes, maxSizeBytes, maxFiles, accept, getUploadParams, autoUpload, validate } = this.props
377
+
378
+ const uploadedDate = new Date().toISOString()
379
+ const lastModifiedDate = lastModified && new Date(lastModified).toISOString()
380
+ const fileWithMeta = {
381
+ file,
382
+ meta: { name, size, type, lastModifiedDate, uploadedDate, percent: 0, id },
383
+ } as IFileWithMeta
384
+
385
+ // firefox versions prior to 53 return a bogus mime type for file drag events,
386
+ // so files with that mime type are always accepted
387
+ if (file.type !== 'application/x-moz-file' && !accepts(file, accept)) {
388
+ fileWithMeta.meta.status = 'rejected_file_type'
389
+ this.handleChangeStatus(fileWithMeta)
390
+ return
391
+ }
392
+ if (this.files.length >= maxFiles) {
393
+ fileWithMeta.meta.status = 'rejected_max_files'
394
+ this.handleChangeStatus(fileWithMeta)
395
+ return
396
+ }
397
+
398
+ fileWithMeta.cancel = () => this.handleCancel(fileWithMeta)
399
+ fileWithMeta.remove = () => this.handleRemove(fileWithMeta)
400
+ fileWithMeta.restart = () => this.handleRestart(fileWithMeta)
401
+
402
+ fileWithMeta.meta.status = 'preparing'
403
+ this.files.push(fileWithMeta)
404
+ this.handleChangeStatus(fileWithMeta)
405
+ this.forceUpdate()
406
+
407
+ if (size < minSizeBytes || size > maxSizeBytes) {
408
+ fileWithMeta.meta.status = 'error_file_size'
409
+ this.handleChangeStatus(fileWithMeta)
410
+ this.forceUpdate()
411
+ return
412
+ }
413
+
414
+ await this.generatePreview(fileWithMeta)
415
+
416
+ if (validate) {
417
+ const error = validate(fileWithMeta)
418
+ if (error) {
419
+ fileWithMeta.meta.status = 'error_validation'
420
+ fileWithMeta.meta.validationError = error // usually a string, but doesn't have to be
421
+ this.handleChangeStatus(fileWithMeta)
422
+ this.forceUpdate()
423
+ return
424
+ }
425
+ }
426
+
427
+ if (getUploadParams) {
428
+ if (autoUpload) {
429
+ this.uploadFile(fileWithMeta)
430
+ fileWithMeta.meta.status = 'getting_upload_params'
431
+ } else {
432
+ fileWithMeta.meta.status = 'ready'
433
+ }
434
+ } else {
435
+ fileWithMeta.meta.status = 'done'
436
+ }
437
+ this.handleChangeStatus(fileWithMeta)
438
+ this.forceUpdate()
439
+ }
440
+
441
+ generatePreview = async (fileWithMeta: IFileWithMeta) => {
442
+ const {
443
+ meta: { type },
444
+ file,
445
+ } = fileWithMeta
446
+ const isImage = type.startsWith('image/')
447
+ const isAudio = type.startsWith('audio/')
448
+ const isVideo = type.startsWith('video/')
449
+ if (!isImage && !isAudio && !isVideo) return
450
+
451
+ const objectUrl = URL.createObjectURL(file)
452
+
453
+ const fileCallbackToPromise = (fileObj: HTMLImageElement | HTMLAudioElement) => {
454
+ return Promise.race([
455
+ new Promise(resolve => {
456
+ if (fileObj instanceof HTMLImageElement) fileObj.onload = resolve
457
+ else fileObj.onloadedmetadata = resolve
458
+ }),
459
+ new Promise((_, reject) => {
460
+ setTimeout(reject, 1000)
461
+ }),
462
+ ])
463
+ }
464
+
465
+ try {
466
+ if (isImage) {
467
+ const img = new Image()
468
+ img.src = objectUrl
469
+ fileWithMeta.meta.previewUrl = objectUrl
470
+ await fileCallbackToPromise(img)
471
+ fileWithMeta.meta.width = img.width
472
+ fileWithMeta.meta.height = img.height
473
+ }
474
+
475
+ if (isAudio) {
476
+ const audio = new Audio()
477
+ audio.src = objectUrl
478
+ await fileCallbackToPromise(audio)
479
+ fileWithMeta.meta.duration = audio.duration
480
+ }
481
+
482
+ if (isVideo) {
483
+ const video = document.createElement('video')
484
+ video.src = objectUrl
485
+ await fileCallbackToPromise(video)
486
+ fileWithMeta.meta.duration = video.duration
487
+ fileWithMeta.meta.videoWidth = video.videoWidth
488
+ fileWithMeta.meta.videoHeight = video.videoHeight
489
+ }
490
+ if (!isImage) URL.revokeObjectURL(objectUrl)
491
+ } catch (e) {
492
+ URL.revokeObjectURL(objectUrl)
493
+ }
494
+ this.forceUpdate()
495
+ }
496
+
497
+ uploadFile = async (fileWithMeta: IFileWithMeta) => {
498
+ const { getUploadParams } = this.props
499
+ if (!getUploadParams) return
500
+ let params: IUploadParams | null = null
501
+ try {
502
+ params = await getUploadParams(fileWithMeta)
503
+ } catch (e) {
504
+ console.error('Error Upload Params', e.stack)
505
+ }
506
+ if (params === null) return
507
+ const { url, method = 'POST', body, fields = {}, headers = {}, meta: extraMeta = {} } = params
508
+ delete extraMeta.status
509
+
510
+ if (!url) {
511
+ fileWithMeta.meta.status = 'error_upload_params'
512
+ this.handleChangeStatus(fileWithMeta)
513
+ this.forceUpdate()
514
+ return
515
+ }
516
+
517
+ const xhr = new XMLHttpRequest()
518
+ const formData = new FormData()
519
+ xhr.open(method, url, true)
520
+
521
+ for (const field of Object.keys(fields)) formData.append(field, fields[field])
522
+ xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest')
523
+ for (const header of Object.keys(headers)) xhr.setRequestHeader(header, headers[header])
524
+ fileWithMeta.meta = { ...fileWithMeta.meta, ...extraMeta }
525
+
526
+ // update progress (can be used to show progress indicator)
527
+ xhr.upload.addEventListener('progress', e => {
528
+ fileWithMeta.meta.percent = (e.loaded * 100.0) / e.total || 100
529
+ this.forceUpdate()
530
+ })
531
+
532
+ xhr.addEventListener('readystatechange', () => {
533
+ // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState
534
+ if (xhr.readyState !== 2 && xhr.readyState !== 4) return
535
+
536
+ if (xhr.status === 0 && fileWithMeta.meta.status !== 'aborted') {
537
+ fileWithMeta.meta.status = 'exception_upload'
538
+ this.handleChangeStatus(fileWithMeta)
539
+ this.forceUpdate()
540
+ }
541
+
542
+ if (xhr.status > 0 && xhr.status < 400) {
543
+ fileWithMeta.meta.percent = 100
544
+ if (xhr.readyState === 2) fileWithMeta.meta.status = 'headers_received'
545
+ if (xhr.readyState === 4) fileWithMeta.meta.status = 'done'
546
+ this.handleChangeStatus(fileWithMeta)
547
+ this.forceUpdate()
548
+ }
549
+
550
+ if (xhr.status >= 400 && fileWithMeta.meta.status !== 'error_upload') {
551
+ fileWithMeta.meta.status = 'error_upload'
552
+ this.handleChangeStatus(fileWithMeta)
553
+ this.forceUpdate()
554
+ }
555
+ })
556
+
557
+ formData.append('file', fileWithMeta.file)
558
+ if (this.props.timeout) xhr.timeout = this.props.timeout
559
+ xhr.send(body || formData)
560
+ fileWithMeta.xhr = xhr
561
+ fileWithMeta.meta.status = 'uploading'
562
+ this.handleChangeStatus(fileWithMeta)
563
+ this.forceUpdate()
564
+ }
565
+
566
+ render() {
567
+ const {
568
+ accept,
569
+ multiple,
570
+ maxFiles,
571
+ minSizeBytes,
572
+ maxSizeBytes,
573
+ onSubmit,
574
+ getUploadParams,
575
+ disabled,
576
+ canCancel,
577
+ canRemove,
578
+ canRestart,
579
+ inputContent,
580
+ inputWithFilesContent,
581
+ submitButtonDisabled,
582
+ submitButtonContent,
583
+ classNames,
584
+ styles,
585
+ addClassNames,
586
+ InputComponent,
587
+ PreviewComponent,
588
+ SubmitButtonComponent,
589
+ LayoutComponent,
590
+ } = this.props
591
+
592
+ const { active, dragged } = this.state
593
+
594
+ const reject = dragged.some(file => file.type !== 'application/x-moz-file' && !accepts(file as File, accept))
595
+ const extra = { active, reject, dragged, accept, multiple, minSizeBytes, maxSizeBytes, maxFiles } as IExtra
596
+ const files = [...this.files]
597
+ const dropzoneDisabled = resolveValue(disabled, files, extra)
598
+
599
+ const {
600
+ classNames: {
601
+ dropzone: dropzoneClassName,
602
+ dropzoneActive: dropzoneActiveClassName,
603
+ dropzoneReject: dropzoneRejectClassName,
604
+ dropzoneDisabled: dropzoneDisabledClassName,
605
+ input: inputClassName,
606
+ inputLabel: inputLabelClassName,
607
+ inputLabelWithFiles: inputLabelWithFilesClassName,
608
+ preview: previewClassName,
609
+ previewImage: previewImageClassName,
610
+ submitButtonContainer: submitButtonContainerClassName,
611
+ submitButton: submitButtonClassName,
612
+ },
613
+ styles: {
614
+ dropzone: dropzoneStyle,
615
+ dropzoneActive: dropzoneActiveStyle,
616
+ dropzoneReject: dropzoneRejectStyle,
617
+ dropzoneDisabled: dropzoneDisabledStyle,
618
+ input: inputStyle,
619
+ inputLabel: inputLabelStyle,
620
+ inputLabelWithFiles: inputLabelWithFilesStyle,
621
+ preview: previewStyle,
622
+ previewImage: previewImageStyle,
623
+ submitButtonContainer: submitButtonContainerStyle,
624
+ submitButton: submitButtonStyle,
625
+ },
626
+ } = mergeStyles(classNames, styles, addClassNames, files, extra)
627
+
628
+ const Input = InputComponent || InputDefault
629
+ const Preview = PreviewComponent || PreviewDefault
630
+ const SubmitButton = SubmitButtonComponent || SubmitButtonDefault
631
+ const Layout = LayoutComponent || LayoutDefault
632
+
633
+ let previews = null
634
+ if (PreviewComponent !== null) {
635
+ previews = files.map(f => {
636
+ return (
637
+ //@ts-ignore
638
+ <Preview
639
+ className={previewClassName}
640
+ imageClassName={previewImageClassName}
641
+ style={previewStyle as React.CSSProperties}
642
+ imageStyle={previewImageStyle as React.CSSProperties}
643
+ key={f.meta.id}
644
+ fileWithMeta={f}
645
+ meta={{ ...f.meta }}
646
+ isUpload={Boolean(getUploadParams)}
647
+ canCancel={resolveValue(canCancel, files, extra)}
648
+ canRemove={resolveValue(canRemove, files, extra)}
649
+ canRestart={resolveValue(canRestart, files, extra)}
650
+ files={files}
651
+ extra={extra}
652
+ />
653
+ )
654
+ })
655
+ }
656
+
657
+ const input =
658
+ InputComponent !== null ? (
659
+ //@ts-ignore
660
+ <Input
661
+ className={inputClassName}
662
+ labelClassName={inputLabelClassName}
663
+ labelWithFilesClassName={inputLabelWithFilesClassName}
664
+ style={inputStyle as React.CSSProperties}
665
+ labelStyle={inputLabelStyle as React.CSSProperties}
666
+ labelWithFilesStyle={inputLabelWithFilesStyle as React.CSSProperties}
667
+ getFilesFromEvent={this.getFilesFromEvent() as IInputProps['getFilesFromEvent']}
668
+ accept={accept}
669
+ multiple={multiple}
670
+ disabled={dropzoneDisabled}
671
+ content={resolveValue(inputContent, files, extra)}
672
+ withFilesContent={resolveValue(inputWithFilesContent, files, extra)}
673
+ onFiles={this.handleFiles} // see: https://stackoverflow.com/questions/39484895
674
+ files={files}
675
+ extra={extra}
676
+ />
677
+ ) : null
678
+
679
+ const submitButton =
680
+ onSubmit && SubmitButtonComponent !== null ? (
681
+ //@ts-ignore
682
+ <SubmitButton
683
+ className={submitButtonContainerClassName}
684
+ buttonClassName={submitButtonClassName}
685
+ style={submitButtonContainerStyle as React.CSSProperties}
686
+ buttonStyle={submitButtonStyle as React.CSSProperties}
687
+ disabled={resolveValue(submitButtonDisabled, files, extra)}
688
+ content={resolveValue(submitButtonContent, files, extra)}
689
+ onSubmit={this.handleSubmit}
690
+ files={files}
691
+ extra={extra}
692
+ />
693
+ ) : null
694
+
695
+ let className = dropzoneClassName
696
+ let style = dropzoneStyle
697
+
698
+ if (dropzoneDisabled) {
699
+ className = `${className} ${dropzoneDisabledClassName}`
700
+ style = { ...(style || {}), ...(dropzoneDisabledStyle || {}) }
701
+ } else if (reject) {
702
+ className = `${className} ${dropzoneRejectClassName}`
703
+ style = { ...(style || {}), ...(dropzoneRejectStyle || {}) }
704
+ } else if (active) {
705
+ className = `${className} ${dropzoneActiveClassName}`
706
+ style = { ...(style || {}), ...(dropzoneActiveStyle || {}) }
707
+ }
708
+
709
+ return (
710
+ //@ts-ignore
711
+ <Layout
712
+ input={input}
713
+ previews={previews}
714
+ submitButton={submitButton}
715
+ dropzoneProps={{
716
+ ref: this.dropzone,
717
+ className,
718
+ style: style as React.CSSProperties,
719
+ onDragEnter: this.handleDragEnter,
720
+ onDragOver: this.handleDragOver,
721
+ onDragLeave: this.handleDragLeave,
722
+ onDrop: dropzoneDisabled ? this.handleDropDisabled : this.handleDrop,
723
+ }}
724
+ files={files}
725
+ extra={
726
+ {
727
+ ...extra,
728
+ onFiles: this.handleFiles,
729
+ onCancelFile: this.handleCancel,
730
+ onRemoveFile: this.handleRemove,
731
+ onRestartFile: this.handleRestart,
732
+ } as IExtraLayout
733
+ }
734
+ />
735
+ )
736
+ }
737
+ }
738
+
739
+ Dropzone.defaultProps = {
740
+ accept: '*',
741
+ multiple: true,
742
+ minSizeBytes: 0,
743
+ maxSizeBytes: Number.MAX_SAFE_INTEGER,
744
+ maxFiles: Number.MAX_SAFE_INTEGER,
745
+ autoUpload: true,
746
+ disabled: false,
747
+ canCancel: true,
748
+ canRemove: true,
749
+ canRestart: true,
750
+ inputContent: 'Drag Files or Click to Browse',
751
+ inputWithFilesContent: 'Add Files',
752
+ submitButtonDisabled: false,
753
+ submitButtonContent: 'Submit',
754
+ classNames: {},
755
+ styles: {},
756
+ addClassNames: {},
757
+ }
758
+
759
+ // @ts-ignore
760
+ Dropzone.propTypes = {
761
+ onChangeStatus: PropTypes.func,
762
+ getUploadParams: PropTypes.func,
763
+ onSubmit: PropTypes.func,
764
+
765
+ getFilesFromEvent: PropTypes.func,
766
+ getDataTransferItemsFromEvent: PropTypes.func,
767
+
768
+ accept: PropTypes.string,
769
+ multiple: PropTypes.bool,
770
+ minSizeBytes: PropTypes.number.isRequired,
771
+ maxSizeBytes: PropTypes.number.isRequired,
772
+ maxFiles: PropTypes.number.isRequired,
773
+
774
+ validate: PropTypes.func,
775
+
776
+ autoUpload: PropTypes.bool,
777
+ timeout: PropTypes.number,
778
+
779
+ initialFiles: PropTypes.arrayOf(PropTypes.any),
780
+
781
+ /* component customization */
782
+ disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
783
+
784
+ canCancel: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
785
+ canRemove: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
786
+ canRestart: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
787
+
788
+ inputContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
789
+ inputWithFilesContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
790
+
791
+ submitButtonDisabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
792
+ submitButtonContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
793
+
794
+ classNames: PropTypes.object.isRequired,
795
+ styles: PropTypes.object.isRequired,
796
+ addClassNames: PropTypes.object.isRequired,
797
+
798
+ /* component injection */
799
+ InputComponent: PropTypes.func,
800
+ PreviewComponent: PropTypes.func,
801
+ SubmitButtonComponent: PropTypes.func,
802
+ LayoutComponent: PropTypes.func,
803
+ }
804
+
805
+ export default Dropzone
806
+ export {
807
+ LayoutDefault as Layout,
808
+ InputDefault as Input,
809
+ PreviewDefault as Preview,
810
+ SubmitButtonDefault as SubmitButton,
811
+ formatBytes,
812
+ formatDuration,
813
+ accepts,
814
+ defaultClassNames,
815
+ defaultGetFilesFromEvent as getFilesFromEvent,
816
+ }