diff --git a/.babelrc b/.babelrc index 258cd87..d761866 100644 --- a/.babelrc +++ b/.babelrc @@ -1,12 +1,12 @@ -{ - "presets": [ - "@babel/env", - "@babel/react" - ], - "plugins": [ - "@babel/plugin-proposal-function-bind", - "@babel/plugin-proposal-object-rest-spread", - "@babel/plugin-proposal-class-properties", - "@babel/plugin-transform-runtime" - ] -} +{ + "presets": [ + "@babel/env", + "@babel/react" + ], + "plugins": [ + "@babel/plugin-proposal-function-bind", + "@babel/plugin-proposal-object-rest-spread", + "@babel/plugin-proposal-class-properties", + "@babel/plugin-transform-runtime" + ] +} diff --git a/.gitattributes b/.gitattributes index 07247e3..2ee963f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,5 @@ -package-lock.json binary -yarn.lock binary -bundle.js binary -docs/assets/styleguide/build/* binary -docs/assets/styleguide-quickstart/build/* binary +package-lock.json binary +yarn.lock binary +bundle.js binary +docs/assets/styleguide/build/* binary +docs/assets/styleguide-quickstart/build/* binary diff --git a/.gitignore b/.gitignore index 1b75394..f316d5f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,20 @@ -# dependencies -/node_modules -/website/node_modules - -## docs -/website/i18n -/website/build - -# production -/dist - -# jest -/coverage - -examples/src/index.js - -# misc -.DS_Store -.env - -**/*.js -**/*.d.ts - -package-lock.json +# dependencies +/node_modules +/website/node_modules + +## docs +/website/i18n +/website/build + +# jest +/coverage + +examples/src/index.js + +# misc +.DS_Store +.env + +package-lock.json + +.history/ \ No newline at end of file diff --git a/.npmignore b/.npmignore index ee6e494..3d13bcc 100644 --- a/.npmignore +++ b/.npmignore @@ -1,28 +1,28 @@ -.babelrc -.eslintrc.js -.eslintignore -.gitignore -.gitattributes -package-lock.json -styleguide.config.js -styleguide-quickstart.config.js -styleguide.setup.js -update_styleguides.py -webpack.config.js -webpack.build.config.js - -examples -node_modules -src - -website -docs - -tests -coverage - -.*.md - -*.gif - -.DS_Store +.babelrc +.eslintrc.js +.eslintignore +.gitignore +.gitattributes +package-lock.json +styleguide.config.js +styleguide-quickstart.config.js +styleguide.setup.js +update_styleguides.py +webpack.config.js +webpack.build.config.js + +examples +node_modules +src + +website +docs + +tests +coverage + +.*.md + +*.gif + +.DS_Store diff --git a/.prettierrc.js b/.prettierrc.js index 7d235f9..0d7a702 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,6 +1,6 @@ -module.exports = { - singleQuote: true, - semi: false, - trailingComma: 'all', - printWidth: 120, -} +module.exports = { + singleQuote: true, + semi: false, + trailingComma: 'all', + printWidth: 120, +} diff --git a/LICENSE b/LICENSE index 3a66f47..56d6753 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -The MIT License (MIT) - -Copyright (c) 2018 Kyle Bebak - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +The MIT License (MIT) + +Copyright (c) 2018 Kyle Bebak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 09ef1b7..5751910 100644 --- a/README.md +++ b/README.md @@ -1,123 +1,123 @@ -# React Dropzone Uploader - - -[![NPM](https://img.shields.io/npm/v/react-dropzone-uploader.svg)](https://www.npmjs.com/package/react-dropzone-uploader) -[![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/react-dropzone-uploader.svg)](https://www.npmjs.com/package/react-dropzone-uploader) - -React Dropzone Uploader is a customizable file dropzone and uploader for React. - - -## Features -- Detailed file metadata and previews, especially for image, video and audio files -- Upload status and progress, upload cancellation and restart -- Easily set auth headers and additional upload fields ([see S3 examples](https://react-dropzone-uploader.js.org/docs/s3)) -- Customize styles using CSS or JS -- Take full control of rendering with component injection props -- Take control of upload lifecycle -- Modular design; use as standalone dropzone, file input, or file uploader -- Cross-browser support, mobile friendly, including direct uploads from camera -- Lightweight and fast -- Excellent TypeScript definitions - -![](https://raw.githubusercontent.com/fortana-co/react-dropzone-uploader/master/rdu.gif) - - -## Documentation - - - -## Installation -`npm install --save react-dropzone-uploader` - -Import default styles in your app. - -~~~js -import 'react-dropzone-uploader/dist/styles.css' -~~~ - - -## Quick Start -RDU handles common use cases with almost no config. The following code gives you a dropzone and clickable file input that accepts image, audio and video files. It uploads files to `https://httpbin.org/post`, and renders a button to submit files that are done uploading. [Check out a live demo](https://react-dropzone-uploader.js.org/docs/quick-start). - -~~~js -import 'react-dropzone-uploader/dist/styles.css' -import Dropzone from 'react-dropzone-uploader' - -const MyUploader = () => { - // specify upload params and url for your files - const getUploadParams = ({ meta }) => { return { url: 'https://httpbin.org/post' } } - - // called every time a file's `status` changes - const handleChangeStatus = ({ meta, file }, status) => { console.log(status, meta, file) } - - // receives array of files that are done uploading when submit button is clicked - const handleSubmit = (files) => { console.log(files.map(f => f.meta)) } - - return ( - - ) -} -~~~ - - -## Examples -See more live examples here: . - - -## Props -Check out [the full table of RDU's props](https://react-dropzone-uploader.js.org/docs/props). - - -## Browser Support -| Chrome | Firefox | Edge | Safari | IE | iOS Safari | Chrome for Android | -| --- | --- | --- | --- | --- | --- | --- | -| ✔ | ✔ | ✔ | 10+, 9\* | 11\* | ✔ | ✔ | - -\* requires `Promise` polyfill, e.g. [@babel/polyfill](https://babeljs.io/docs/en/babel-polyfill) - - -## UMD Build -This library is available as an ES Module at . - -If you want to include it in your page, you need to include the dependencies and CSS as well. - -~~~html - - - - - - -~~~ - - -## Contributing -There are a number of places RDU could be improved; [see here](https://github.com/fortana-co/react-dropzone-uploader/labels/help%20wanted). - -For example, RDU has solid core functionality, but has a minimalist look and feel. It would be more beginner-friendly with a larger variety of built-in components. - - -### Shout Outs -Thanks to @nchen63 for helping with [TypeScript defs](https://github.com/fortana-co/react-dropzone-uploader/blob/master/src/Dropzone.d.ts)! - - -### Running Dev -Clone the project, install dependencies, and run the dev server. - -~~~sh -git clone git://github.com/fortana-co/react-dropzone-uploader.git -cd react-dropzone-uploader -yarn -npm run dev -~~~ - -This runs code in `examples/src/index.js`, which has many examples that use `Dropzone`. The library source code is in the `/src` directory. - - -## Thanks -Thanks to `react-dropzone`, `react-select`, and `redux-form` for inspiration. +# React Dropzone Uploader + + +[![NPM](https://img.shields.io/npm/v/react-dropzone-uploader.svg)](https://www.npmjs.com/package/react-dropzone-uploader) +[![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/react-dropzone-uploader.svg)](https://www.npmjs.com/package/react-dropzone-uploader) + +React Dropzone Uploader is a customizable file dropzone and uploader for React. + + +## Features +- Detailed file metadata and previews, especially for image, video and audio files +- Upload status and progress, upload cancellation and restart +- Easily set auth headers and additional upload fields ([see S3 examples](https://react-dropzone-uploader.js.org/docs/s3)) +- Customize styles using CSS or JS +- Take full control of rendering with component injection props +- Take control of upload lifecycle +- Modular design; use as standalone dropzone, file input, or file uploader +- Cross-browser support, mobile friendly, including direct uploads from camera +- Lightweight and fast +- Excellent TypeScript definitions + +![](https://raw.githubusercontent.com/fortana-co/react-dropzone-uploader/master/rdu.gif) + + +## Documentation + + + +## Installation +`npm install --save react-dropzone-uploader` + +Import default styles in your app. + +~~~js +import 'react-dropzone-uploader/dist/styles.css' +~~~ + + +## Quick Start +RDU handles common use cases with almost no config. The following code gives you a dropzone and clickable file input that accepts image, audio and video files. It uploads files to `https://httpbin.org/post`, and renders a button to submit files that are done uploading. [Check out a live demo](https://react-dropzone-uploader.js.org/docs/quick-start). + +~~~js +import 'react-dropzone-uploader/dist/styles.css' +import Dropzone from 'react-dropzone-uploader' + +const MyUploader = () => { + // specify upload params and url for your files + const getUploadParams = ({ meta }) => { return { url: 'https://httpbin.org/post' } } + + // called every time a file's `status` changes + const handleChangeStatus = ({ meta, file }, status) => { console.log(status, meta, file) } + + // receives array of files that are done uploading when submit button is clicked + const handleSubmit = (files) => { console.log(files.map(f => f.meta)) } + + return ( + + ) +} +~~~ + + +## Examples +See more live examples here: . + + +## Props +Check out [the full table of RDU's props](https://react-dropzone-uploader.js.org/docs/props). + + +## Browser Support +| Chrome | Firefox | Edge | Safari | IE | iOS Safari | Chrome for Android | +| --- | --- | --- | --- | --- | --- | --- | +| ✔ | ✔ | ✔ | 10+, 9\* | 11\* | ✔ | ✔ | + +\* requires `Promise` polyfill, e.g. [@babel/polyfill](https://babeljs.io/docs/en/babel-polyfill) + + +## UMD Build +This library is available as an ES Module at . + +If you want to include it in your page, you need to include the dependencies and CSS as well. + +~~~html + + + + + + +~~~ + + +## Contributing +There are a number of places RDU could be improved; [see here](https://github.com/fortana-co/react-dropzone-uploader/labels/help%20wanted). + +For example, RDU has solid core functionality, but has a minimalist look and feel. It would be more beginner-friendly with a larger variety of built-in components. + + +### Shout Outs +Thanks to @nchen63 for helping with [TypeScript defs](https://github.com/fortana-co/react-dropzone-uploader/blob/master/src/Dropzone.d.ts)! + + +### Running Dev +Clone the project, install dependencies, and run the dev server. + +~~~sh +git clone git://github.com/fortana-co/react-dropzone-uploader.git +cd react-dropzone-uploader +yarn +npm run dev +~~~ + +This runs code in `examples/src/index.js`, which has many examples that use `Dropzone`. The library source code is in the `/src` directory. + + +## Thanks +Thanks to `react-dropzone`, `react-select`, and `redux-form` for inspiration. diff --git a/build_docs.sh b/build_docs.sh index af0741e..90ecc77 100755 --- a/build_docs.sh +++ b/build_docs.sh @@ -1,3 +1,3 @@ -npm run build-styleguide -npm run build-styleguide-quickstart -python3 update_styleguides.py +npm run build-styleguide +npm run build-styleguide-quickstart +python3 update_styleguides.py diff --git a/dist/Dropzone.tsx b/dist/Dropzone.tsx new file mode 100644 index 0000000..1ff0ef7 --- /dev/null +++ b/dist/Dropzone.tsx @@ -0,0 +1,818 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import LayoutDefault from './Layout' +import InputDefault from './Input' +import PreviewDefault from './Preview' +import SubmitButtonDefault from './SubmitButton' +import { + formatBytes, + formatDuration, + accepts, + resolveValue, + mergeStyles, + defaultClassNames, + getFilesFromEvent as defaultGetFilesFromEvent, +} from './utils' + +export type StatusValue = + | 'rejected_file_type' + | 'rejected_max_files' + | 'preparing' + | 'error_file_size' + | 'error_validation' + | 'ready' + | 'started' + | 'getting_upload_params' + | 'error_upload_params' + | 'uploading' + | 'exception_upload' + | 'aborted' + | 'restarted' + | 'removed' + | 'error_upload' + | 'headers_received' + | 'done' + +export type MethodValue = + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'DELETE' + | 'GET' + | 'HEAD' + | 'OPTIONS' + | 'PATCH' + | 'POST' + | 'PUT' + +export interface IMeta { + id: string + status: StatusValue + type: string // MIME type, example: `image/*` + name: string + uploadedDate: string // ISO string + percent: number + size: number // bytes + lastModifiedDate: string // ISO string + previewUrl?: string // from URL.createObjectURL + duration?: number // seconds + width?: number + height?: number + videoWidth?: number + videoHeight?: number + validationError?: any +} + +export interface IFileWithMeta { + file: File + meta: IMeta + cancel: () => void + restart: () => void + remove: () => void + xhr?: XMLHttpRequest +} + +export interface IExtra { + active: boolean + reject: boolean + dragged: DataTransferItem[] + accept: string + multiple: boolean + minSizeBytes: number + maxSizeBytes: number + maxFiles: number +} + +export interface IUploadParams { + url: string + method?: MethodValue + body?: string | FormData | ArrayBuffer | Blob | File | URLSearchParams + fields?: { [name: string]: string | Blob } + headers?: { [name: string]: string } + meta?: { [name: string]: any } + withCredentials?: boolean +} + +export type CustomizationFunction = (allFiles: IFileWithMeta[], extra: IExtra) => T + +export interface IStyleCustomization { + dropzone?: T | CustomizationFunction + dropzoneActive?: T | CustomizationFunction + dropzoneReject?: T | CustomizationFunction + dropzoneDisabled?: T | CustomizationFunction + input?: T | CustomizationFunction + inputLabel?: T | CustomizationFunction + inputLabelWithFiles?: T | CustomizationFunction + preview?: T | CustomizationFunction + previewImage?: T | CustomizationFunction + submitButtonContainer?: T | CustomizationFunction + submitButton?: T | CustomizationFunction +} + +export interface IExtraLayout extends IExtra { + onFiles(files: File[]): void + onCancelFile(file: IFileWithMeta): void + onRemoveFile(file: IFileWithMeta): void + onRestartFile(file: IFileWithMeta): void +} + +export interface ILayoutProps { + files: IFileWithMeta[] + extra: IExtraLayout + input: React.ReactNode + previews: React.ReactNode[] | null + submitButton: React.ReactNode + dropzoneProps: { + ref: React.RefObject + className: string + style?: React.CSSProperties + onDragEnter(event: React.DragEvent): void + onDragOver(event: React.DragEvent): void + onDragLeave(event: React.DragEvent): void + onDrop(event: React.DragEvent): void + } +} + +interface ICommonProps { + files: IFileWithMeta[] + extra: IExtra +} + +export interface IPreviewProps extends ICommonProps { + meta: IMeta + className?: string + imageClassName?: string + style?: React.CSSProperties + imageStyle?: React.CSSProperties + fileWithMeta: IFileWithMeta + isUpload: boolean + canCancel: boolean + canRemove: boolean + canRestart: boolean +} + +export interface IInputProps extends ICommonProps { + className?: string + labelClassName?: string + labelWithFilesClassName?: string + style?: React.CSSProperties + labelStyle?: React.CSSProperties + labelWithFilesStyle?: React.CSSProperties + getFilesFromEvent: (event: React.ChangeEvent) => Promise + accept: string + multiple: boolean + disabled: boolean + content?: React.ReactNode + withFilesContent?: React.ReactNode + onFiles: (files: File[]) => void +} + +export interface ISubmitButtonProps extends ICommonProps { + className?: string + buttonClassName?: string + style?: React.CSSProperties + buttonStyle?: React.CSSProperties + disabled: boolean + content?: React.ReactNode + onSubmit: (files: IFileWithMeta[]) => void +} + +type ReactComponent = (props: Props) => React.ReactNode | React.Component + +export interface IDropzoneProps { + onChangeStatus?( + file: IFileWithMeta, + status: StatusValue, + allFiles: IFileWithMeta[], + ): { meta: { [name: string]: any } } | void + getUploadParams?(file: IFileWithMeta): IUploadParams | Promise + onSubmit?(successFiles: IFileWithMeta[], allFiles: IFileWithMeta[]): void + + getFilesFromEvent?: ( + event: React.DragEvent | React.ChangeEvent, + ) => Promise | File[] + getDataTransferItemsFromEvent?: ( + event: React.DragEvent, + ) => Promise | DataTransferItem[] + + accept: string + multiple: boolean + minSizeBytes: number + maxSizeBytes: number + maxFiles: number + + validate?(file: IFileWithMeta): any // usually a string, but can be anything + + autoUpload: boolean + timeout?: number + + initialFiles?: File[] + + /* component customization */ + disabled: boolean | CustomizationFunction + + canCancel: boolean | CustomizationFunction + canRemove: boolean | CustomizationFunction + canRestart: boolean | CustomizationFunction + + inputContent: React.ReactNode | CustomizationFunction + inputWithFilesContent: React.ReactNode | CustomizationFunction + + submitButtonDisabled: boolean | CustomizationFunction + submitButtonContent: React.ReactNode | CustomizationFunction + + classNames: IStyleCustomization + styles: IStyleCustomization + addClassNames: IStyleCustomization + + /* component injection */ + LayoutComponent?: ReactComponent + PreviewComponent?: ReactComponent + InputComponent?: ReactComponent + SubmitButtonComponent?: ReactComponent +} + +class Dropzone extends React.Component { + static defaultProps: IDropzoneProps + protected files: IFileWithMeta[] + protected mounted: boolean + protected dropzone: React.RefObject + protected dragTimeoutId?: number + + constructor(props: IDropzoneProps) { + super(props) + this.state = { + active: false, + dragged: [], + } + this.files = [] + this.mounted = true + this.dropzone = React.createRef() + } + + componentDidMount() { + if (this.props.initialFiles) this.handleFiles(this.props.initialFiles) + } + + componentDidUpdate(prevProps: IDropzoneProps) { + const { initialFiles } = this.props + if (prevProps.initialFiles !== initialFiles && initialFiles) this.handleFiles(initialFiles) + } + + componentWillUnmount() { + this.mounted = false + for (const fileWithMeta of this.files) this.handleCancel(fileWithMeta) + } + + forceUpdate = () => { + if (this.mounted) super.forceUpdate() + } + + getFilesFromEvent = () => { + return this.props.getFilesFromEvent || defaultGetFilesFromEvent + } + + getDataTransferItemsFromEvent = () => { + return this.props.getDataTransferItemsFromEvent || defaultGetFilesFromEvent + } + + handleDragEnter = async (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + const dragged = (await this.getDataTransferItemsFromEvent()(e)) as DataTransferItem[] + this.setState({ active: true, dragged }) + } + + handleDragOver = async (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + clearTimeout(this.dragTimeoutId) + const dragged = await this.getDataTransferItemsFromEvent()(e) + this.setState({ active: true, dragged }) + } + + handleDragLeave = (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + // prevents repeated toggling of `active` state when file is dragged over children of uploader + // see: https://www.smashingmagazine.com/2018/01/drag-drop-file-uploader-vanilla-js/ + this.dragTimeoutId = window.setTimeout(() => this.setState({ active: false, dragged: [] }), 150) + } + + handleDrop = async (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + this.setState({ active: false, dragged: [] }) + const files = (await this.getFilesFromEvent()(e)) as File[] + this.handleFiles(files) + } + + handleDropDisabled = (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + this.setState({ active: false, dragged: [] }) + } + + handleChangeStatus = (fileWithMeta: IFileWithMeta) => { + if (!this.props.onChangeStatus) return + const { meta = {} } = this.props.onChangeStatus(fileWithMeta, fileWithMeta.meta.status, this.files) || {} + if (meta) { + delete meta.status + fileWithMeta.meta = { ...fileWithMeta.meta, ...meta } + this.forceUpdate() + } + } + + handleSubmit = (files: IFileWithMeta[]) => { + if (this.props.onSubmit) this.props.onSubmit(files, [...this.files]) + } + + handleCancel = (fileWithMeta: IFileWithMeta) => { + if (fileWithMeta.meta.status !== 'uploading') return + fileWithMeta.meta.status = 'aborted' + if (fileWithMeta.xhr) fileWithMeta.xhr.abort() + this.handleChangeStatus(fileWithMeta) + this.forceUpdate() + } + + handleRemove = (fileWithMeta: IFileWithMeta) => { + const index = this.files.findIndex(f => f === fileWithMeta) + if (index !== -1) { + URL.revokeObjectURL(fileWithMeta.meta.previewUrl || '') + fileWithMeta.meta.status = 'removed' + this.handleChangeStatus(fileWithMeta) + this.files.splice(index, 1) + this.forceUpdate() + } + } + + handleRestart = (fileWithMeta: IFileWithMeta) => { + if (!this.props.getUploadParams) return + + if (fileWithMeta.meta.status === 'ready') fileWithMeta.meta.status = 'started' + else fileWithMeta.meta.status = 'restarted' + this.handleChangeStatus(fileWithMeta) + + fileWithMeta.meta.status = 'getting_upload_params' + fileWithMeta.meta.percent = 0 + this.handleChangeStatus(fileWithMeta) + this.forceUpdate() + this.uploadFile(fileWithMeta) + } + + // expects an array of File objects + handleFiles = (files: File[]) => { + files.forEach((f, i) => this.handleFile(f, `${new Date().getTime()}-${i}`)) + const { current } = this.dropzone + if (current) setTimeout(() => current.scroll({ top: current.scrollHeight, behavior: 'smooth' }), 150) + } + + handleFile = async (file: File, id: string) => { + const { name, size, type, lastModified } = file + const { minSizeBytes, maxSizeBytes, maxFiles, accept, getUploadParams, autoUpload, validate } = this.props + + const uploadedDate = new Date().toISOString() + const lastModifiedDate = lastModified && new Date(lastModified).toISOString() + const fileWithMeta = { + file, + meta: { name, size, type, lastModifiedDate, uploadedDate, percent: 0, id }, + } as IFileWithMeta + + // firefox versions prior to 53 return a bogus mime type for file drag events, + // so files with that mime type are always accepted + if (file.type !== 'application/x-moz-file' && !accepts(file, accept)) { + fileWithMeta.meta.status = 'rejected_file_type' + this.handleChangeStatus(fileWithMeta) + return + } + if (this.files.length >= maxFiles) { + fileWithMeta.meta.status = 'rejected_max_files' + this.handleChangeStatus(fileWithMeta) + return + } + + fileWithMeta.cancel = () => this.handleCancel(fileWithMeta) + fileWithMeta.remove = () => this.handleRemove(fileWithMeta) + fileWithMeta.restart = () => this.handleRestart(fileWithMeta) + + fileWithMeta.meta.status = 'preparing' + this.files.push(fileWithMeta) + this.handleChangeStatus(fileWithMeta) + this.forceUpdate() + + if (size < minSizeBytes || size > maxSizeBytes) { + fileWithMeta.meta.status = 'error_file_size' + this.handleChangeStatus(fileWithMeta) + this.forceUpdate() + return + } + + await this.generatePreview(fileWithMeta) + + if (validate) { + const error = validate(fileWithMeta) + if (error) { + fileWithMeta.meta.status = 'error_validation' + fileWithMeta.meta.validationError = error // usually a string, but doesn't have to be + this.handleChangeStatus(fileWithMeta) + this.forceUpdate() + return + } + } + + if (getUploadParams) { + if (autoUpload) { + this.uploadFile(fileWithMeta) + fileWithMeta.meta.status = 'getting_upload_params' + } else { + fileWithMeta.meta.status = 'ready' + } + } else { + fileWithMeta.meta.status = 'done' + } + this.handleChangeStatus(fileWithMeta) + this.forceUpdate() + } + + generatePreview = async (fileWithMeta: IFileWithMeta) => { + const { + meta: { type }, + file, + } = fileWithMeta + const isImage = type.startsWith('image/') + const isAudio = type.startsWith('audio/') + const isVideo = type.startsWith('video/') + if (!isImage && !isAudio && !isVideo) return + + const objectUrl = URL.createObjectURL(file) + + const fileCallbackToPromise = (fileObj: HTMLImageElement | HTMLAudioElement) => { + return Promise.race([ + new Promise(resolve => { + if (fileObj instanceof HTMLImageElement) fileObj.onload = resolve + else fileObj.onloadedmetadata = resolve + }), + new Promise((_, reject) => { + setTimeout(reject, 1000) + }), + ]) + } + + try { + if (isImage) { + const img = new Image() + img.src = objectUrl + fileWithMeta.meta.previewUrl = objectUrl + await fileCallbackToPromise(img) + fileWithMeta.meta.width = img.width + fileWithMeta.meta.height = img.height + } + + if (isAudio) { + const audio = new Audio() + audio.src = objectUrl + await fileCallbackToPromise(audio) + fileWithMeta.meta.duration = audio.duration + } + + if (isVideo) { + const video = document.createElement('video') + video.src = objectUrl + await fileCallbackToPromise(video) + fileWithMeta.meta.duration = video.duration + fileWithMeta.meta.videoWidth = video.videoWidth + fileWithMeta.meta.videoHeight = video.videoHeight + } + if (!isImage) URL.revokeObjectURL(objectUrl) + } catch (e) { + URL.revokeObjectURL(objectUrl) + } + this.forceUpdate() + } + + uploadFile = async (fileWithMeta: IFileWithMeta) => { + const { getUploadParams } = this.props + if (!getUploadParams) return + let params: IUploadParams | null = null + try { + params = await getUploadParams(fileWithMeta) + } catch (e) { + console.error('Error Upload Params', e.stack) + } + if (params === null) return + const { url, method = 'POST', body, fields = {}, headers = {}, meta: extraMeta = {} } = params + delete extraMeta.status + + if (!url) { + fileWithMeta.meta.status = 'error_upload_params' + this.handleChangeStatus(fileWithMeta) + this.forceUpdate() + return + } + + const xhr = new XMLHttpRequest() + if (params.withCredentials) xhr.withCredentials = true + const formData = new FormData() + xhr.open(method, url, true) + + for (const field of Object.keys(fields)) formData.append(field, fields[field]) + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest') + for (const header of Object.keys(headers)) xhr.setRequestHeader(header, headers[header]) + fileWithMeta.meta = { ...fileWithMeta.meta, ...extraMeta } + + // update progress (can be used to show progress indicator) + xhr.upload.addEventListener('progress', e => { + fileWithMeta.meta.percent = (e.loaded * 100.0) / e.total || 100 + this.forceUpdate() + }) + + xhr.addEventListener('readystatechange', () => { + // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState + if (xhr.readyState !== 2 && xhr.readyState !== 4) return + + if (xhr.status === 0 && fileWithMeta.meta.status !== 'aborted') { + fileWithMeta.meta.status = 'exception_upload' + this.handleChangeStatus(fileWithMeta) + this.forceUpdate() + } + + if (xhr.status > 0 && xhr.status < 400) { + fileWithMeta.meta.percent = 100 + if (xhr.readyState === 2) fileWithMeta.meta.status = 'headers_received' + if (xhr.readyState === 4) fileWithMeta.meta.status = 'done' + this.handleChangeStatus(fileWithMeta) + this.forceUpdate() + } + + if (xhr.status >= 400 && fileWithMeta.meta.status !== 'error_upload') { + fileWithMeta.meta.status = 'error_upload' + this.handleChangeStatus(fileWithMeta) + this.forceUpdate() + } + }) + + formData.append('file', fileWithMeta.file) + if (this.props.timeout) xhr.timeout = this.props.timeout + xhr.send(body || formData) + fileWithMeta.xhr = xhr + fileWithMeta.meta.status = 'uploading' + this.handleChangeStatus(fileWithMeta) + this.forceUpdate() + } + + render() { + const { + accept, + multiple, + maxFiles, + minSizeBytes, + maxSizeBytes, + onSubmit, + getUploadParams, + disabled, + canCancel, + canRemove, + canRestart, + inputContent, + inputWithFilesContent, + submitButtonDisabled, + submitButtonContent, + classNames, + styles, + addClassNames, + InputComponent, + PreviewComponent, + SubmitButtonComponent, + LayoutComponent, + } = this.props + + const { active, dragged } = this.state + + const reject = dragged.some(file => file.type !== 'application/x-moz-file' && !accepts(file as File, accept)) + const extra = { active, reject, dragged, accept, multiple, minSizeBytes, maxSizeBytes, maxFiles } as IExtra + const files = [...this.files] + const dropzoneDisabled = resolveValue(disabled, files, extra) + + const { + classNames: { + dropzone: dropzoneClassName, + dropzoneActive: dropzoneActiveClassName, + dropzoneReject: dropzoneRejectClassName, + dropzoneDisabled: dropzoneDisabledClassName, + input: inputClassName, + inputLabel: inputLabelClassName, + inputLabelWithFiles: inputLabelWithFilesClassName, + preview: previewClassName, + previewImage: previewImageClassName, + submitButtonContainer: submitButtonContainerClassName, + submitButton: submitButtonClassName, + }, + styles: { + dropzone: dropzoneStyle, + dropzoneActive: dropzoneActiveStyle, + dropzoneReject: dropzoneRejectStyle, + dropzoneDisabled: dropzoneDisabledStyle, + input: inputStyle, + inputLabel: inputLabelStyle, + inputLabelWithFiles: inputLabelWithFilesStyle, + preview: previewStyle, + previewImage: previewImageStyle, + submitButtonContainer: submitButtonContainerStyle, + submitButton: submitButtonStyle, + }, + } = mergeStyles(classNames, styles, addClassNames, files, extra) + + const Input = InputComponent || InputDefault + const Preview = PreviewComponent || PreviewDefault + const SubmitButton = SubmitButtonComponent || SubmitButtonDefault + const Layout = LayoutComponent || LayoutDefault + + let previews = null + if (PreviewComponent !== null) { + previews = files.map(f => { + return ( + //@ts-ignore + + ) + }) + } + + const input = + InputComponent !== null ? ( + //@ts-ignore + + ) : null + + const submitButton = + onSubmit && SubmitButtonComponent !== null ? ( + //@ts-ignore + + ) : null + + let className = dropzoneClassName + let style = dropzoneStyle + + if (dropzoneDisabled) { + className = `${className} ${dropzoneDisabledClassName}` + style = { ...(style || {}), ...(dropzoneDisabledStyle || {}) } + } else if (reject) { + className = `${className} ${dropzoneRejectClassName}` + style = { ...(style || {}), ...(dropzoneRejectStyle || {}) } + } else if (active) { + className = `${className} ${dropzoneActiveClassName}` + style = { ...(style || {}), ...(dropzoneActiveStyle || {}) } + } + + return ( + //@ts-ignore + + ) + } +} + +Dropzone.defaultProps = { + accept: '*', + multiple: true, + minSizeBytes: 0, + maxSizeBytes: Number.MAX_SAFE_INTEGER, + maxFiles: Number.MAX_SAFE_INTEGER, + autoUpload: true, + disabled: false, + canCancel: true, + canRemove: true, + canRestart: true, + inputContent: 'Drag Files or Click to Browse', + inputWithFilesContent: 'Add Files', + submitButtonDisabled: false, + submitButtonContent: 'Submit', + classNames: {}, + styles: {}, + addClassNames: {}, +} + +// @ts-ignore +Dropzone.propTypes = { + onChangeStatus: PropTypes.func, + getUploadParams: PropTypes.func, + onSubmit: PropTypes.func, + + getFilesFromEvent: PropTypes.func, + getDataTransferItemsFromEvent: PropTypes.func, + + accept: PropTypes.string, + multiple: PropTypes.bool, + minSizeBytes: PropTypes.number.isRequired, + maxSizeBytes: PropTypes.number.isRequired, + maxFiles: PropTypes.number.isRequired, + + validate: PropTypes.func, + + autoUpload: PropTypes.bool, + timeout: PropTypes.number, + + initialFiles: PropTypes.arrayOf(PropTypes.any), + + /* component customization */ + disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), + + canCancel: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), + canRemove: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), + canRestart: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), + + inputContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + inputWithFilesContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + + submitButtonDisabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), + submitButtonContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + + classNames: PropTypes.object.isRequired, + styles: PropTypes.object.isRequired, + addClassNames: PropTypes.object.isRequired, + + /* component injection */ + InputComponent: PropTypes.func, + PreviewComponent: PropTypes.func, + SubmitButtonComponent: PropTypes.func, + LayoutComponent: PropTypes.func, +} + +export default Dropzone +export { + LayoutDefault as Layout, + InputDefault as Input, + PreviewDefault as Preview, + SubmitButtonDefault as SubmitButton, + formatBytes, + formatDuration, + accepts, + defaultClassNames, + defaultGetFilesFromEvent as getFilesFromEvent, +} diff --git a/dist/Input.tsx b/dist/Input.tsx new file mode 100644 index 0000000..09e3f13 --- /dev/null +++ b/dist/Input.tsx @@ -0,0 +1,76 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import { IInputProps } from './Dropzone' + +const Input = (props: IInputProps) => { + const { + className, + labelClassName, + labelWithFilesClassName, + style, + labelStyle, + labelWithFilesStyle, + getFilesFromEvent, + accept, + multiple, + disabled, + content, + withFilesContent, + onFiles, + files, + } = props + + return ( + + ) +} + +Input.propTypes = { + className: PropTypes.string, + labelClassName: PropTypes.string, + labelWithFilesClassName: PropTypes.string, + style: PropTypes.object, + labelStyle: PropTypes.object, + labelWithFilesStyle: PropTypes.object, + getFilesFromEvent: PropTypes.func.isRequired, + accept: PropTypes.string.isRequired, + multiple: PropTypes.bool.isRequired, + disabled: PropTypes.bool.isRequired, + content: PropTypes.node, + withFilesContent: PropTypes.node, + onFiles: PropTypes.func.isRequired, + files: PropTypes.arrayOf(PropTypes.any).isRequired, + extra: PropTypes.shape({ + active: PropTypes.bool.isRequired, + reject: PropTypes.bool.isRequired, + dragged: PropTypes.arrayOf(PropTypes.any).isRequired, + accept: PropTypes.string.isRequired, + multiple: PropTypes.bool.isRequired, + minSizeBytes: PropTypes.number.isRequired, + maxSizeBytes: PropTypes.number.isRequired, + maxFiles: PropTypes.number.isRequired, + }).isRequired, +} + +export default Input diff --git a/dist/Layout.tsx b/dist/Layout.tsx new file mode 100644 index 0000000..a9e52f4 --- /dev/null +++ b/dist/Layout.tsx @@ -0,0 +1,57 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import { ILayoutProps } from './Dropzone' + +const Layout = (props: ILayoutProps) => { + const { + input, + previews, + submitButton, + dropzoneProps, + files, + extra: { maxFiles }, + } = props + + return ( +
+ {previews} + + {files.length < maxFiles && input} + + {files.length > 0 && submitButton} +
+ ) +} + +Layout.propTypes = { + input: PropTypes.node, + previews: PropTypes.arrayOf(PropTypes.node), + submitButton: PropTypes.node, + dropzoneProps: PropTypes.shape({ + ref: PropTypes.any.isRequired, + className: PropTypes.string.isRequired, + style: PropTypes.object, + onDragEnter: PropTypes.func.isRequired, + onDragOver: PropTypes.func.isRequired, + onDragLeave: PropTypes.func.isRequired, + onDrop: PropTypes.func.isRequired, + }).isRequired, + files: PropTypes.arrayOf(PropTypes.any).isRequired, + extra: PropTypes.shape({ + active: PropTypes.bool.isRequired, + reject: PropTypes.bool.isRequired, + dragged: PropTypes.arrayOf(PropTypes.any).isRequired, + accept: PropTypes.string.isRequired, + multiple: PropTypes.bool.isRequired, + minSizeBytes: PropTypes.number.isRequired, + maxSizeBytes: PropTypes.number.isRequired, + maxFiles: PropTypes.number.isRequired, + onFiles: PropTypes.func.isRequired, + onCancelFile: PropTypes.func.isRequired, + onRemoveFile: PropTypes.func.isRequired, + onRestartFile: PropTypes.func.isRequired, + }).isRequired, +} + +export default Layout diff --git a/dist/Preview.tsx b/dist/Preview.tsx new file mode 100644 index 0000000..ddc9772 --- /dev/null +++ b/dist/Preview.tsx @@ -0,0 +1,139 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import { formatBytes, formatDuration } from './utils' +import { IPreviewProps } from './Dropzone' +//@ts-ignore +import cancelImg from './assets/cancel.svg' +//@ts-ignore +import removeImg from './assets/remove.svg' +//@ts-ignore +import restartImg from './assets/restart.svg' + +const iconByFn = { + cancel: { backgroundImage: `url(${cancelImg})` }, + remove: { backgroundImage: `url(${removeImg})` }, + restart: { backgroundImage: `url(${restartImg})` }, +} + +class Preview extends React.PureComponent { + render() { + const { + className, + imageClassName, + style, + imageStyle, + fileWithMeta: { cancel, remove, restart }, + meta: { name = '', percent = 0, size = 0, previewUrl, status, duration, validationError }, + isUpload, + canCancel, + canRemove, + canRestart, + extra: { minSizeBytes }, + } = this.props + + let title = `${name || '?'}, ${formatBytes(size)}` + if (duration) title = `${title}, ${formatDuration(duration)}` + + if (status === 'error_file_size' || status === 'error_validation') { + return ( +
+ {title} + {status === 'error_file_size' && {size < minSizeBytes ? 'File too small' : 'File too big'}} + {status === 'error_validation' && {String(validationError)}} + {canRemove && } +
+ ) + } + + if (status === 'error_upload_params' || status === 'exception_upload' || status === 'error_upload') { + title = `${title} (upload failed)` + } + if (status === 'aborted') title = `${title} (cancelled)` + + return ( +
+ {previewUrl && {title}} + {!previewUrl && {title}} + +
+ {isUpload && ( + + )} + + {status === 'uploading' && canCancel && ( + + )} + {status !== 'preparing' && status !== 'getting_upload_params' && status !== 'uploading' && canRemove && ( + + )} + {['error_upload_params', 'exception_upload', 'error_upload', 'aborted', 'ready'].includes(status) && + canRestart && } +
+
+ ) + } +} + +// @ts-ignore +Preview.propTypes = { + className: PropTypes.string, + imageClassName: PropTypes.string, + style: PropTypes.object, + imageStyle: PropTypes.object, + fileWithMeta: PropTypes.shape({ + file: PropTypes.any.isRequired, + meta: PropTypes.object.isRequired, + cancel: PropTypes.func.isRequired, + restart: PropTypes.func.isRequired, + remove: PropTypes.func.isRequired, + xhr: PropTypes.any, + }).isRequired, + // copy of fileWithMeta.meta, won't be mutated + meta: PropTypes.shape({ + status: PropTypes.oneOf([ + 'preparing', + 'error_file_size', + 'error_validation', + 'ready', + 'getting_upload_params', + 'error_upload_params', + 'uploading', + 'exception_upload', + 'aborted', + 'error_upload', + 'headers_received', + 'done', + ]).isRequired, + type: PropTypes.string.isRequired, + name: PropTypes.string, + uploadedDate: PropTypes.string.isRequired, + percent: PropTypes.number, + size: PropTypes.number, + lastModifiedDate: PropTypes.string, + previewUrl: PropTypes.string, + duration: PropTypes.number, + width: PropTypes.number, + height: PropTypes.number, + videoWidth: PropTypes.number, + videoHeight: PropTypes.number, + validationError: PropTypes.any, + }).isRequired, + isUpload: PropTypes.bool.isRequired, + canCancel: PropTypes.bool.isRequired, + canRemove: PropTypes.bool.isRequired, + canRestart: PropTypes.bool.isRequired, + files: PropTypes.arrayOf(PropTypes.any).isRequired, // eslint-disable-line react/no-unused-prop-types + extra: PropTypes.shape({ + active: PropTypes.bool.isRequired, + reject: PropTypes.bool.isRequired, + dragged: PropTypes.arrayOf(PropTypes.any).isRequired, + accept: PropTypes.string.isRequired, + multiple: PropTypes.bool.isRequired, + minSizeBytes: PropTypes.number.isRequired, + maxSizeBytes: PropTypes.number.isRequired, + maxFiles: PropTypes.number.isRequired, + }).isRequired, +} + +export default Preview diff --git a/dist/SubmitButton.tsx b/dist/SubmitButton.tsx new file mode 100644 index 0000000..8468714 --- /dev/null +++ b/dist/SubmitButton.tsx @@ -0,0 +1,47 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import { ISubmitButtonProps } from './Dropzone' + +const SubmitButton = (props: ISubmitButtonProps) => { + const { className, buttonClassName, style, buttonStyle, disabled, content, onSubmit, files } = props + + const _disabled = + files.some(f => ['preparing', 'getting_upload_params', 'uploading'].includes(f.meta.status)) || + !files.some(f => ['headers_received', 'done'].includes(f.meta.status)) + + const handleSubmit = () => { + onSubmit(files.filter(f => ['headers_received', 'done'].includes(f.meta.status))) + } + + return ( +
+ +
+ ) +} + +SubmitButton.propTypes = { + className: PropTypes.string, + buttonClassName: PropTypes.string, + style: PropTypes.object, + buttonStyle: PropTypes.object, + disabled: PropTypes.bool.isRequired, + content: PropTypes.node, + onSubmit: PropTypes.func.isRequired, + files: PropTypes.arrayOf(PropTypes.object).isRequired, + extra: PropTypes.shape({ + active: PropTypes.bool.isRequired, + reject: PropTypes.bool.isRequired, + dragged: PropTypes.arrayOf(PropTypes.any).isRequired, + accept: PropTypes.string.isRequired, + multiple: PropTypes.bool.isRequired, + minSizeBytes: PropTypes.number.isRequired, + maxSizeBytes: PropTypes.number.isRequired, + maxFiles: PropTypes.number.isRequired, + }).isRequired, +} + +export default SubmitButton diff --git a/dist/react-dropzone-uploader.js b/dist/react-dropzone-uploader.js new file mode 100644 index 0000000..0f546b3 --- /dev/null +++ b/dist/react-dropzone-uploader.js @@ -0,0 +1,14 @@ +module.exports=function(r){var n={};function a(e){if(n[e])return n[e].exports;var t=n[e]={i:e,l:!1,exports:{}};return r[e].call(t.exports,t,t.exports,a),t.l=!0,t.exports}return a.m=r,a.c=n,a.d=function(e,t,r){a.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(t,e){if(1&e&&(t=a(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(a.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var n in t)a.d(r,n,function(e){return t[e]}.bind(null,n));return r},a.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(t,"a",t),t},a.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},a.p="",a(a.s=32)}([function(e,t,r){e.exports=r(27)()},function(e,t,r){"use strict";e.exports=r(25)},function(e,t,r){e.exports=r(21)},function(e,t,r){var a=r(20);e.exports=function(t){for(var e=1;e=l)return v.meta.status="rejected_max_files",C.handleChangeStatus(v),e.abrupt("return");e.next=13;break;case 13:if(v.cancel=function(){return C.handleCancel(v)},v.remove=function(){return C.handleRemove(v)},v.restart=function(){return C.handleRestart(v)},v.meta.status="preparing",C.files.push(v),C.handleChangeStatus(v),C.forceUpdate(),a=l)return v.meta.status="rejected_max_files",C.handleChangeStatus(v),e.abrupt("return");e.next=13;break;case 13:if(v.cancel=function(){return C.handleCancel(v)},v.remove=function(){return C.handleRemove(v)},v.restart=function(){return C.handleRestart(v)},v.meta.status="preparing",C.files.push(v),C.handleChangeStatus(v),C.forceUpdate(),a { + const units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + let l = 0 + let n = b + + while (n >= 1024) { + n /= 1024 + l += 1 + } + + return `${n.toFixed(n >= 10 || l < 1 ? 0 : 1)}${units[l]}` +} + +export const formatDuration = (seconds: number) => { + const date = new Date(0) + date.setSeconds(seconds) + const dateString = date.toISOString().slice(11, 19) + if (seconds < 3600) return dateString.slice(3) + return dateString +} + +// adapted from: https://github.com/okonet/attr-accept/blob/master/src/index.js +// returns true if file.name is empty and accept string is something like ".csv", +// because file comes from dataTransferItem for drag events, and +// dataTransferItem.name is always empty +export const accepts = (file: File, accept: string) => { + if (!accept || accept === '*') return true + + const mimeType = file.type || '' + const baseMimeType = mimeType.replace(/\/.*$/, '') + + return accept + .split(',') + .map(t => t.trim()) + .some(type => { + if (type.charAt(0) === '.') { + return file.name === undefined || file.name.toLowerCase().endsWith(type.toLowerCase()) + } else if (type.endsWith('/*')) { + // this is something like an image/* mime type + return baseMimeType === type.replace(/\/.*$/, '') + } + return mimeType === type + }) +} + +type ResolveFn = (...args: any[]) => T + +export const resolveValue = (value: ResolveFn | T, ...args: any[]) => { + if (typeof value === 'function') return (value as ResolveFn)(...args) + return value +} + +export const defaultClassNames = { + dropzone: 'dzu-dropzone', + dropzoneActive: 'dzu-dropzoneActive', + dropzoneReject: 'dzu-dropzoneActive', + dropzoneDisabled: 'dzu-dropzoneDisabled', + input: 'dzu-input', + inputLabel: 'dzu-inputLabel', + inputLabelWithFiles: 'dzu-inputLabelWithFiles', + preview: 'dzu-previewContainer', + previewImage: 'dzu-previewImage', + submitButtonContainer: 'dzu-submitButtonContainer', + submitButton: 'dzu-submitButton', +} + +export const mergeStyles = ( + classNames: IStyleCustomization, + styles: IStyleCustomization, + addClassNames: IStyleCustomization, + ...args: any[] +) => { + const resolvedClassNames: { [property: string]: string } = { ...defaultClassNames } + const resolvedStyles = { ...styles } as { [property: string]: string } + + for (const [key, value] of Object.entries(classNames)) { + resolvedClassNames[key] = resolveValue(value, ...args) + } + + for (const [key, value] of Object.entries(addClassNames)) { + resolvedClassNames[key] = `${resolvedClassNames[key]} ${resolveValue(value, ...args)}` + } + + for (const [key, value] of Object.entries(styles)) { + resolvedStyles[key] = resolveValue(value, ...args) + } + + return { classNames: resolvedClassNames, styles: resolvedStyles as IStyleCustomization } +} + +export const getFilesFromEvent = ( + event: React.DragEvent | React.ChangeEvent, +): Array => { + let items = null + + if ('dataTransfer' in event) { + const dt = event.dataTransfer + + // NOTE: Only the 'drop' event has access to DataTransfer.files, otherwise it will always be empty + if ('files' in dt && dt.files.length) { + items = dt.files + } else if (dt.items && dt.items.length) { + items = dt.items + } + } else if (event.target && event.target.files) { + items = event.target.files + } + + return Array.prototype.slice.call(items) +} diff --git a/docs/api.md b/docs/api.md index 308c18b..86ef9fc 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,133 +1,133 @@ ---- -id: api -title: API ---- - - -__RDU is modular__. In the Quick Start example, the only prop needed to perform uploads is `getUploadParams`. `onChangeStatus` is included to show how a file's status changes as it's dropped and uploaded. `onSubmit` gives users a button to submit files that are done uploading. - -Want to disable the file input? Pass `null` for `InputComponent`. Don't want to show file previews? Pass `null` for `PreviewComponent`. Don't need a submit button after files are uploaded? Pass `null` for `SubmitButtonComponent`, or simply omit the `onSubmit` prop. - -Don't want to upload files? Omit `getUploadParams`, and you'll have a dropzone that calls `onChangeStatus` every time you add a file. This callback receives a `fileWithMeta` object and the file's `status`. If status is `'done'`, the file has been prepared and validated. Add it to an array of accepted files, or do whatever you want with it. - - -## `onChangeStatus` -This is called every time a file's status changes: `fileWithMeta.meta.status`. - -It receives `(fileWithMeta, status, []fileWithMeta)`. The first argument is the `fileWithMeta` object whose status changed, while the third argument is the array of all `fileWithMeta` objects being tracked by the dropzone. - -Returning a `meta` object from this callback lets you merge new values into the file's `meta`. - ->`onChangeStatus` is never called repeatedly for the same status. - - -### Status Values -Here are all possible values for `fileWithMeta.meta.status`. - -- `'rejected_file_type'` - + set because of `accept` prop; file not added to dropzone's file array -- `'rejected_max_files'` - + set because of `maxFiles` prop; file not added to dropzone's file array -- `'preparing'` - + set before file validation and preview generation -- `'error_file_size'` - + set because of `minSizeBytes` and/or `maxSizeBytes` props -- `'error_validation'` - + set if you pass `validate` function and it returns falsy value -- `'ready'` - + only set if you pass `autoUpload={false}`; set when file has been prepared and validated; client code can call `fileWithMeta.restart` to start upload -- `'started'` - + set if status is `'ready'` and user starts upload, or client code calls `fileWithMeta.restart` -- `'getting_upload_params'` - + set after file is prepared, right before `getUploadParams` is called -- `'error_upload_params'` - + set if you pass `getUploadParams` and it throws an exception, or it doesn't return `{ url: '...' }` -- `'uploading'` - + set right after `xhr.send` is called and xhr is set on `fileWithMeta` -- `'exception_upload'` - + set if upload times out or there is no connection to upload server -- `'aborted'` - + set if `fileWithMeta.meta.status` is `'uploading'` and user aborts upload, or client code calls `fileWithMeta.cancel` -- `'restarted'` - + set if user restarts upload, or client code calls `fileWithMeta.restart` -- `'removed'` - + set if user removes file from dropzone, or client code calls `fileWithMeta.remove` -- `'error_upload'` - + set if upload response has HTTP [status code >= 400](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/status) -- `'headers_received'` - + set for successful upload when [xhr.readyState is HEADERS_RECEIVED](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState); headers are available but response body is not -- `'done'` - + set for successful upload when [xhr.readyState is DONE](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState); response body is available - + if you don't pass a `getUploadParams` function because you're not using RDU to upload files, this is set after file is prepared and validated - - -## `getUploadParams` -`getUploadParams` is a callback that receives a `fileWithMeta` object and returns the params needed to upload the file. If this prop isn't passed, RDU doesn't initiate and manage file uploads. - -`getUploadParams` can be async, in case you need to go over the network to get upload params for a file. It should return an object with `{ url (string), method (string), body, fields (object), headers (object), meta (object) }`. - -The only required key is `url`. __POST__ is the default method. `headers` sets headers using `XMLHttpRequest.setRequestHeader`, which makes it easy to authenticate with the upload server. - -If you pass your own [request `body`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/send#Syntax), RDU uploads it using [xhr.send](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/send). - -~~~js -const getUploadParams = ({ file, meta }) => { - const body = new FormData() - body.append('fileField', file) - return { url: 'https://httpbin.org/post', body } -} -~~~ - -If you don't, RDU creates the request body for you. It creates a `FormData` instance and appends the file to it using `formData.append('file', fileWithMeta.file)`. `fields` lets you [append additional fields to the FormData instance](https://developer.mozilla.org/en-US/docs/Web/API/FormData/append). - -Returning a `meta` object from this callback lets you merge new values into the file's `meta`. - - -## `onSubmit` -This is called when a user presses the submit button. - -It receives `([]fileWithMeta, []fileWithMeta)`. The first argument is an array of `fileWithMeta` objects whose status is `'headers_received'` or `'done'`. The second argument is the array of __all__ `fileWithMeta` objects being tracked by the dropzone. - -If you omit this prop the dropzone doesn't render a submit button. - - -## `fileWithMeta` Objects -RDU maintains an array of files it's currently tracking and rendering. The elements of this array are `fileWithMeta` objects, which contain the following keys: - -- `file` - + [file instance](https://developer.mozilla.org/en-US/docs/Web/API/File) returned by `onDrop` event or by input's `onChange` event -- `meta` - + file metadata, containing a subset of the following keys: `status`, `type`, `name`, `uploadedDate`, `percent`, `size`, `lastModifiedDate`, `previewUrl`, `duration`, `width`, `height`, `videoWidth`, `videoHeight`, `id` -- `cancel`, `restart`, `remove` - + functions that allow client code to take control of the upload lifecycle; cancel file upload, (re)start file upload, or remove file from dropzone -- `xhr` - + instance of `XMLHttpRequest` if the file is being uploaded, else undefined - -RDU's callback props `onChangeStatus`, `getUploadParams`, `onSubmit` and `validate` receive single or multiple `fileWithMeta` objects. - -These objects give you all the metadata you need to create a customized, reactive file dropzone, file input, or file uploader. - - -### Mutability -Note that `fileWithMeta` objects __are mutable__. If you mutate them, RDU may behave unexpectedly, so don't do this! - ->This is why `onChangeStatus` also receives `status` instead of just `fileWithMeta`. Your callback gets the correct, immutable value of `status`, even if `fileWithMeta.meta.status` is later updated. - -`getUploadParams` and `onChangeStatus` have an API for safely merging new values into a file's meta. If you return something like `{ meta: { newKey: newValue } }` from these functions, RDU merges the new values into the file's `meta`. - - -## Accepted Files -To control which files can be dropped or picked, you can use the `accept` prop, which is really the [HTML5 input accept attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#Limiting_accepted_file_types). Also available are the `minSizeBytes`, `maxSizeBytes` and `maxFiles` props. - -Files whose sizes fall outside the range `[minSizeBytes, maxSizeBytes]` are rendered in the dropzone with a special error status. Files rejected because they don't have the correct type, or because they exceed your max number of files, call `onChangeStatus` with special status values, but are not rendered. - -If you need totally custom filter logic, you can pass a generic `validate` function. This function receives a `fileWithMeta` object. If you return a falsy value from `validate`, the file is accepted, else it's rejected. - - -## Utility Functions -In case you want to use them, RDU exports the following utility functions: - -- `formatBytes(bytes: number): string` -- `formatDuration(seconds: number): string` -- `accepts(file: { name?: string; type?: string }, accept?: string): boolean` -- `getFilesFromEvent(event: React.DragEvent | React.ChangeEvent): Array` +--- +id: api +title: API +--- + + +__RDU is modular__. In the Quick Start example, the only prop needed to perform uploads is `getUploadParams`. `onChangeStatus` is included to show how a file's status changes as it's dropped and uploaded. `onSubmit` gives users a button to submit files that are done uploading. + +Want to disable the file input? Pass `null` for `InputComponent`. Don't want to show file previews? Pass `null` for `PreviewComponent`. Don't need a submit button after files are uploaded? Pass `null` for `SubmitButtonComponent`, or simply omit the `onSubmit` prop. + +Don't want to upload files? Omit `getUploadParams`, and you'll have a dropzone that calls `onChangeStatus` every time you add a file. This callback receives a `fileWithMeta` object and the file's `status`. If status is `'done'`, the file has been prepared and validated. Add it to an array of accepted files, or do whatever you want with it. + + +## `onChangeStatus` +This is called every time a file's status changes: `fileWithMeta.meta.status`. + +It receives `(fileWithMeta, status, []fileWithMeta)`. The first argument is the `fileWithMeta` object whose status changed, while the third argument is the array of all `fileWithMeta` objects being tracked by the dropzone. + +Returning a `meta` object from this callback lets you merge new values into the file's `meta`. + +>`onChangeStatus` is never called repeatedly for the same status. + + +### Status Values +Here are all possible values for `fileWithMeta.meta.status`. + +- `'rejected_file_type'` + + set because of `accept` prop; file not added to dropzone's file array +- `'rejected_max_files'` + + set because of `maxFiles` prop; file not added to dropzone's file array +- `'preparing'` + + set before file validation and preview generation +- `'error_file_size'` + + set because of `minSizeBytes` and/or `maxSizeBytes` props +- `'error_validation'` + + set if you pass `validate` function and it returns falsy value +- `'ready'` + + only set if you pass `autoUpload={false}`; set when file has been prepared and validated; client code can call `fileWithMeta.restart` to start upload +- `'started'` + + set if status is `'ready'` and user starts upload, or client code calls `fileWithMeta.restart` +- `'getting_upload_params'` + + set after file is prepared, right before `getUploadParams` is called +- `'error_upload_params'` + + set if you pass `getUploadParams` and it throws an exception, or it doesn't return `{ url: '...' }` +- `'uploading'` + + set right after `xhr.send` is called and xhr is set on `fileWithMeta` +- `'exception_upload'` + + set if upload times out or there is no connection to upload server +- `'aborted'` + + set if `fileWithMeta.meta.status` is `'uploading'` and user aborts upload, or client code calls `fileWithMeta.cancel` +- `'restarted'` + + set if user restarts upload, or client code calls `fileWithMeta.restart` +- `'removed'` + + set if user removes file from dropzone, or client code calls `fileWithMeta.remove` +- `'error_upload'` + + set if upload response has HTTP [status code >= 400](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/status) +- `'headers_received'` + + set for successful upload when [xhr.readyState is HEADERS_RECEIVED](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState); headers are available but response body is not +- `'done'` + + set for successful upload when [xhr.readyState is DONE](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState); response body is available + + if you don't pass a `getUploadParams` function because you're not using RDU to upload files, this is set after file is prepared and validated + + +## `getUploadParams` +`getUploadParams` is a callback that receives a `fileWithMeta` object and returns the params needed to upload the file. If this prop isn't passed, RDU doesn't initiate and manage file uploads. + +`getUploadParams` can be async, in case you need to go over the network to get upload params for a file. It should return an object with `{ url (string), method (string), body, fields (object), headers (object), meta (object) }`. + +The only required key is `url`. __POST__ is the default method. `headers` sets headers using `XMLHttpRequest.setRequestHeader`, which makes it easy to authenticate with the upload server. + +If you pass your own [request `body`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/send#Syntax), RDU uploads it using [xhr.send](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/send). + +~~~js +const getUploadParams = ({ file, meta }) => { + const body = new FormData() + body.append('fileField', file) + return { url: 'https://httpbin.org/post', body } +} +~~~ + +If you don't, RDU creates the request body for you. It creates a `FormData` instance and appends the file to it using `formData.append('file', fileWithMeta.file)`. `fields` lets you [append additional fields to the FormData instance](https://developer.mozilla.org/en-US/docs/Web/API/FormData/append). + +Returning a `meta` object from this callback lets you merge new values into the file's `meta`. + + +## `onSubmit` +This is called when a user presses the submit button. + +It receives `([]fileWithMeta, []fileWithMeta)`. The first argument is an array of `fileWithMeta` objects whose status is `'headers_received'` or `'done'`. The second argument is the array of __all__ `fileWithMeta` objects being tracked by the dropzone. + +If you omit this prop the dropzone doesn't render a submit button. + + +## `fileWithMeta` Objects +RDU maintains an array of files it's currently tracking and rendering. The elements of this array are `fileWithMeta` objects, which contain the following keys: + +- `file` + + [file instance](https://developer.mozilla.org/en-US/docs/Web/API/File) returned by `onDrop` event or by input's `onChange` event +- `meta` + + file metadata, containing a subset of the following keys: `status`, `type`, `name`, `uploadedDate`, `percent`, `size`, `lastModifiedDate`, `previewUrl`, `duration`, `width`, `height`, `videoWidth`, `videoHeight`, `id` +- `cancel`, `restart`, `remove` + + functions that allow client code to take control of the upload lifecycle; cancel file upload, (re)start file upload, or remove file from dropzone +- `xhr` + + instance of `XMLHttpRequest` if the file is being uploaded, else undefined + +RDU's callback props `onChangeStatus`, `getUploadParams`, `onSubmit` and `validate` receive single or multiple `fileWithMeta` objects. + +These objects give you all the metadata you need to create a customized, reactive file dropzone, file input, or file uploader. + + +### Mutability +Note that `fileWithMeta` objects __are mutable__. If you mutate them, RDU may behave unexpectedly, so don't do this! + +>This is why `onChangeStatus` also receives `status` instead of just `fileWithMeta`. Your callback gets the correct, immutable value of `status`, even if `fileWithMeta.meta.status` is later updated. + +`getUploadParams` and `onChangeStatus` have an API for safely merging new values into a file's meta. If you return something like `{ meta: { newKey: newValue } }` from these functions, RDU merges the new values into the file's `meta`. + + +## Accepted Files +To control which files can be dropped or picked, you can use the `accept` prop, which is really the [HTML5 input accept attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#Limiting_accepted_file_types). Also available are the `minSizeBytes`, `maxSizeBytes` and `maxFiles` props. + +Files whose sizes fall outside the range `[minSizeBytes, maxSizeBytes]` are rendered in the dropzone with a special error status. Files rejected because they don't have the correct type, or because they exceed your max number of files, call `onChangeStatus` with special status values, but are not rendered. + +If you need totally custom filter logic, you can pass a generic `validate` function. This function receives a `fileWithMeta` object. If you return a falsy value from `validate`, the file is accepted, else it's rejected. + + +## Utility Functions +In case you want to use them, RDU exports the following utility functions: + +- `formatBytes(bytes: number): string` +- `formatDuration(seconds: number): string` +- `accepts(file: { name?: string; type?: string }, accept?: string): boolean` +- `getFilesFromEvent(event: React.DragEvent | React.ChangeEvent): Array` diff --git a/docs/customization.md b/docs/customization.md index f0904d3..0935669 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -1,107 +1,107 @@ ---- -id: customization -title: Customization ---- - - -Notice the __"Drag Files or Click to Browse"__ text that appears by default in an empty dropzone? This is likely something you'll want to change. You can use the `inputContent` and `inputWithFilesContent` props to render any string or JSX you want. The latter is for the content that's rendered if the dropzone has files. If you'd rather not render file input content, just pass `null`. - -Want to change `submitButtonContent` from its default value of __"Submit"__? Just pass a new string or JSX for this prop. To kill this text, pass an empty string or null. - -See the rest of the component customization props [here](props.md#component-customization-props). - - -## Custom Styles -RDU's default styles are defined using CSS. They can be overridden using the `classNames` and `styles` props, which expose RDU's simple, flexible styling framework. - -Both `classNames` and `styles` should be objects containing a subset of the following keys: - -- `dropzone` - + dropzone wrapper -- `dropzoneActive` - + dropzone wrapper on drag over; __added__ to the __dropzone__ class -- `dropzoneReject` - + dropzone wrapper on drag over if file MIME types in [DataTransfer.items](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/items) don't match `accept` prop; __added__ to the __dropzone__ class -- `dropzoneDisabled` - + dropzone wrapper if dropzone is disabled; __added__ to the __dropzone__ class -- `input` - + input (set to `display: none;` by default) -- `inputLabel` - + input label -- `inputLabelWithFiles` - + input label if dropzone has files -- `preview` - + preview wrapper -- `previewImage` - + preview image -- `submitButtonContainer` - + submit button wrapper -- `submitButton` - + submit button - -Each key points to a [CSS class in the default stylesheet](https://github.com/fortana-co/react-dropzone-uploader/blob/master/src/styles.css). A class can be overridden by pointing its key to a different class name, or it can be removed by pointing its key to the empty string `''`. Note that RDU exports a `defaultClassNames` object, a map from these keys to the CSS class names in the default stylesheet. - -If you prefer to use style object literals instead of CSS classes, point a key to a style object. The style object is passed to the target component's `style` prop, which means it takes precedence over its default class, but doesn't overwrite it. - -To overwrite it, you can remove the default class by passing an empty string inside the `classNames` prop. - ->As with any React component, declaring your `styles` object inside your render method may hurt performance, because it will cause RDU components that use these styles to re-render even if their props haven't changed. - - -### Adding To Default Classes -If you want to merge your class names with RDU's default classes, use the `addClassNames` prop. Added class names work like `classNames`, except instead of overwriting default classes they are added (concatenated) to them. - -You can use both `classNames` and `addClassNames` if you want to overwrite some classes and add to others. - ->Use `addClassNames` to override individual default styles, such as `border`, with your own styles. As long as you import RDU's default stylesheet at the top of your app's root component, you won't have to use `!important`. - - -## Component Customization As A Function Of State -[Component customization props](props.md#component-customization-props), including the strings and object literals in the custom styles props, can also be passed as functions that __react to the state of the dropzone__. - -If, for example, you pass a __func__ instead of a __node__ for `inputContent`, this function receives `(files, extra)`, and should return the __node__ to be rendered. - -`files` is the array of `fileWithMeta` objects tracked by the dropzone, and `extra` is an object with other dropzone state and props. `extra` contains the following keys: `{ active, reject, dragged, accept, multiple, minSizeBytes, maxSizeBytes, maxFiles }`. - - -## Component Injection -If no combination of component customization props achieves the look and feel you want, RDU provides a component injection API as an escape hatch. The API is a variation on the render props pattern, and allows you to take complete control over RDU's UX. - -The `InputComponent`, `PreviewComponent`, `SubmitButtonComponent`, `LayoutComponent` props can each be used to override their corresponding default component. - -These components receive the props they need to react to the current state of the dropzone and its files, including the `files` and `extra` props mentioned above. - -`null`ing these props removes their corresponding components, except for `LayoutComponent`. - -The file input and submit button are simple, and it's usually easy to get the right look and feel without component injection. For the file preview these props might not be enough. In this case you can pass a custom `PreviewComponent`, which should be a React component. The custom component receives the same props that would have been passed to the default component. - - -### Default Components -If you use the component injection API, you'll probably want to copy the default component and modify it. - -You'll also need to know which props are passed to your injected components. Scroll to the bottom of the following files to see their prop types. Or, [if you're using TypeScript](https://react-dropzone-uploader.js.org/docs/typescript), add a type definition to the props received by your custom component and inspect away. - -- [InputComponent](https://github.com/fortana-co/react-dropzone-uploader/blob/master/src/Input.js) -- [PreviewComponent](https://github.com/fortana-co/react-dropzone-uploader/blob/master/src/Preview.js) -- [SubmitButtonComponent](https://github.com/fortana-co/react-dropzone-uploader/blob/master/src/SubmitButton.js) -- [LayoutComponent](https://github.com/fortana-co/react-dropzone-uploader/blob/master/src/Layout.js) - - -### Custom Layout -By default, RDU's layout component renders previews, file input and submit button as children of a dropzone div that responds to drag and drop events. - -If you want to change this layout, e.g. to render the previews and submit button outside of your dropzone, you'll need to pass your own `LayoutComponent`. - -If this sounds daunting you probably haven't looked at [Layout](https://github.com/fortana-co/react-dropzone-uploader/blob/master/src/Layout.js) yet. Layout gets pre-rendered `input`, `previews`, and `submitButton` props, which makes changing RDU's layout trivial. - - -### Pass Additional Props To Injected Components -Component injection props can be [function or class components](https://reactjs.org/docs/components-and-props.html#function-and-class-components). - -A function component is literally a function that accepts a props argument and returns a React element. So, if you want to pass additional props to your injected component, just do something like this: - -~~~js - } -/> -~~~ +--- +id: customization +title: Customization +--- + + +Notice the __"Drag Files or Click to Browse"__ text that appears by default in an empty dropzone? This is likely something you'll want to change. You can use the `inputContent` and `inputWithFilesContent` props to render any string or JSX you want. The latter is for the content that's rendered if the dropzone has files. If you'd rather not render file input content, just pass `null`. + +Want to change `submitButtonContent` from its default value of __"Submit"__? Just pass a new string or JSX for this prop. To kill this text, pass an empty string or null. + +See the rest of the component customization props [here](props.md#component-customization-props). + + +## Custom Styles +RDU's default styles are defined using CSS. They can be overridden using the `classNames` and `styles` props, which expose RDU's simple, flexible styling framework. + +Both `classNames` and `styles` should be objects containing a subset of the following keys: + +- `dropzone` + + dropzone wrapper +- `dropzoneActive` + + dropzone wrapper on drag over; __added__ to the __dropzone__ class +- `dropzoneReject` + + dropzone wrapper on drag over if file MIME types in [DataTransfer.items](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/items) don't match `accept` prop; __added__ to the __dropzone__ class +- `dropzoneDisabled` + + dropzone wrapper if dropzone is disabled; __added__ to the __dropzone__ class +- `input` + + input (set to `display: none;` by default) +- `inputLabel` + + input label +- `inputLabelWithFiles` + + input label if dropzone has files +- `preview` + + preview wrapper +- `previewImage` + + preview image +- `submitButtonContainer` + + submit button wrapper +- `submitButton` + + submit button + +Each key points to a [CSS class in the default stylesheet](https://github.com/fortana-co/react-dropzone-uploader/blob/master/src/styles.css). A class can be overridden by pointing its key to a different class name, or it can be removed by pointing its key to the empty string `''`. Note that RDU exports a `defaultClassNames` object, a map from these keys to the CSS class names in the default stylesheet. + +If you prefer to use style object literals instead of CSS classes, point a key to a style object. The style object is passed to the target component's `style` prop, which means it takes precedence over its default class, but doesn't overwrite it. + +To overwrite it, you can remove the default class by passing an empty string inside the `classNames` prop. + +>As with any React component, declaring your `styles` object inside your render method may hurt performance, because it will cause RDU components that use these styles to re-render even if their props haven't changed. + + +### Adding To Default Classes +If you want to merge your class names with RDU's default classes, use the `addClassNames` prop. Added class names work like `classNames`, except instead of overwriting default classes they are added (concatenated) to them. + +You can use both `classNames` and `addClassNames` if you want to overwrite some classes and add to others. + +>Use `addClassNames` to override individual default styles, such as `border`, with your own styles. As long as you import RDU's default stylesheet at the top of your app's root component, you won't have to use `!important`. + + +## Component Customization As A Function Of State +[Component customization props](props.md#component-customization-props), including the strings and object literals in the custom styles props, can also be passed as functions that __react to the state of the dropzone__. + +If, for example, you pass a __func__ instead of a __node__ for `inputContent`, this function receives `(files, extra)`, and should return the __node__ to be rendered. + +`files` is the array of `fileWithMeta` objects tracked by the dropzone, and `extra` is an object with other dropzone state and props. `extra` contains the following keys: `{ active, reject, dragged, accept, multiple, minSizeBytes, maxSizeBytes, maxFiles }`. + + +## Component Injection +If no combination of component customization props achieves the look and feel you want, RDU provides a component injection API as an escape hatch. The API is a variation on the render props pattern, and allows you to take complete control over RDU's UX. + +The `InputComponent`, `PreviewComponent`, `SubmitButtonComponent`, `LayoutComponent` props can each be used to override their corresponding default component. + +These components receive the props they need to react to the current state of the dropzone and its files, including the `files` and `extra` props mentioned above. + +`null`ing these props removes their corresponding components, except for `LayoutComponent`. + +The file input and submit button are simple, and it's usually easy to get the right look and feel without component injection. For the file preview these props might not be enough. In this case you can pass a custom `PreviewComponent`, which should be a React component. The custom component receives the same props that would have been passed to the default component. + + +### Default Components +If you use the component injection API, you'll probably want to copy the default component and modify it. + +You'll also need to know which props are passed to your injected components. Scroll to the bottom of the following files to see their prop types. Or, [if you're using TypeScript](https://react-dropzone-uploader.js.org/docs/typescript), add a type definition to the props received by your custom component and inspect away. + +- [InputComponent](https://github.com/fortana-co/react-dropzone-uploader/blob/master/src/Input.js) +- [PreviewComponent](https://github.com/fortana-co/react-dropzone-uploader/blob/master/src/Preview.js) +- [SubmitButtonComponent](https://github.com/fortana-co/react-dropzone-uploader/blob/master/src/SubmitButton.js) +- [LayoutComponent](https://github.com/fortana-co/react-dropzone-uploader/blob/master/src/Layout.js) + + +### Custom Layout +By default, RDU's layout component renders previews, file input and submit button as children of a dropzone div that responds to drag and drop events. + +If you want to change this layout, e.g. to render the previews and submit button outside of your dropzone, you'll need to pass your own `LayoutComponent`. + +If this sounds daunting you probably haven't looked at [Layout](https://github.com/fortana-co/react-dropzone-uploader/blob/master/src/Layout.js) yet. Layout gets pre-rendered `input`, `previews`, and `submitButton` props, which makes changing RDU's layout trivial. + + +### Pass Additional Props To Injected Components +Component injection props can be [function or class components](https://reactjs.org/docs/components-and-props.html#function-and-class-components). + +A function component is literally a function that accepts a props argument and returns a React element. So, if you want to pass additional props to your injected component, just do something like this: + +~~~js + } +/> +~~~ diff --git a/docs/examples.md b/docs/examples.md index ea22172..5c2f325 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -1,9 +1,9 @@ ---- -id: examples -title: Live Examples ---- - -You can edit code for any of these examples __and see changes live__. Open your browser's console to see how RDU manages file metadata and the upload lifecycle. - -
- +--- +id: examples +title: Live Examples +--- + +You can edit code for any of these examples __and see changes live__. Open your browser's console to see how RDU manages file metadata and the upload lifecycle. + +
+ diff --git a/docs/props.md b/docs/props.md index f509387..edc14c9 100644 --- a/docs/props.md +++ b/docs/props.md @@ -1,56 +1,56 @@ ---- -id: props -title: Props ---- - - -The following props can be passed to `Dropzone`. - -| Name | Type | Default Value | Description | -| --- | --- | --- | --- | -| onChangeStatus | func | | called every time __fileWithMeta.meta.status__ changes; receives __(fileWithMeta, status, []fileWithMeta)__; possible status values are __'rejected_file_type', 'rejected_max_files', 'preparing', 'error_file_size', 'error_validation', 'ready', 'started', 'getting_upload_params', 'error_upload_params', 'uploading', 'exception_upload', 'aborted', 'restarted', 'removed', 'error_upload', 'headers_received', 'done'__ | -| getUploadParams | func | | called after file is prepared and validated, right before upload; receives __fileWithMeta__ object; should return params needed for upload: __{ fields (object), headers (object), meta (object), method (string), url (string) }__; omit to remove upload functionality from dropzone | -| onSubmit | func | | called when user presses submit button; receives array of __fileWithMeta__ objects whose status is __'headers_received'__ or __'done'__; also receives array of all __fileWithMeta__ objects as second argument; omit to remove submit button | -| accept | string | `'*'` | the accept attribute of the file dropzone/input | -| multiple | bool | `true` | the multiple attribute of the file input | -| minSizeBytes | number | `0` | min file size in bytes (1024 * 1024 = 1MB) | -| maxSizeBytes | number | `2^53 - 1` | max file size in bytes (1024 * 1024 = 1MB) | -| maxFiles | number | `2^53 - 1` | max number of files that can be tracked and rendered by the dropzone | -| validate | func | | generic validation function called after file is prepared; receives __fileWithMeta__ object; should return falsy value if validation succeeds; should return truthy value if validation fails, which sets __meta.status__ to __'error_validation'__, and sets __meta.validationError__ to the returned value | -| autoUpload | bool | `true` | pass false to prevent file from being uploaded automatically; sets __meta.status__ to __'ready'__ (instead of __'getting_upload_params'__) after file is prepared and validated; you can call __fileWithMeta.restart__ whenever you want to initiate file upload | -| timeout | number | | pass an integer to make upload time out after this many ms; if upload times out, onChangeStatus is invoked with value __'exception_upload'__ | -| initialFiles | `File[]` | | add these files to dropzone, without any user interaction; if a new array of `initialFiles` is passed, they will also be added to dropzone; see here for an [example of creating and uploading a file from a data URL](https://react-dropzone-uploader.js.org/docs/examples#initial-file-from-data-url) | - - -## Component Customization Props -| Name | Type | Default Value | Description | -| --- | --- | --- | --- | -| disabled | bool/func | `false` | true to disable dropzone and input | -| canCancel | bool/func | `true` | false to remove cancel button in file preview | -| canRestart | bool/func | `true` | false to remove restart button in file preview | -| canRemove | bool/func | `true` | false to remove remove button in file preview | -| inputContent | node/func | `'Drag Files or Click to Browse'` | child of input __label__; '' or null to remove | -| inputWithFilesContent | node/func | `'Add Files'` | child of input __label__; '' or null to remove | -| submitButtonDisabled | bool/func | `false` | true to disable submit button | -| submitButtonContent | node/func | `'Submit'` | '' or null to remove | -| classNames | object | `{}` | see "Custom Styles" section | -| styles | object | `{}` | see "Custom Styles" section | -| addClassNames | object | `{}` | see "Custom Styles" section | - - -## Component Injection Props -| Name | Type | Default Value | Description | -| --- | --- | --- | --- | -| InputComponent | func | | overrides __Input__; null to remove | -| PreviewComponent | func | | overrides __Preview__; null to remove | -| SubmitButtonComponent | func | | overrides __SubmitButton__; null to remove | -| LayoutComponent | func | | overrides __Layout__; can't be removed | - - -## Props Passed To Injected Components -If you use the component injection API, you'll want to know which props are passed to your injected components. Scroll to the bottom of the following files to see their prop types. - -- [InputComponent](https://github.com/fortana-co/react-dropzone-uploader/blob/master/src/Input.js) -- [PreviewComponent](https://github.com/fortana-co/react-dropzone-uploader/blob/master/src/Preview.js) -- [SubmitButtonComponent](https://github.com/fortana-co/react-dropzone-uploader/blob/master/src/SubmitButton.js) -- [LayoutComponent](https://github.com/fortana-co/react-dropzone-uploader/blob/master/src/Layout.js) +--- +id: props +title: Props +--- + + +The following props can be passed to `Dropzone`. + +| Name | Type | Default Value | Description | +| --- | --- | --- | --- | +| onChangeStatus | func | | called every time __fileWithMeta.meta.status__ changes; receives __(fileWithMeta, status, []fileWithMeta)__; possible status values are __'rejected_file_type', 'rejected_max_files', 'preparing', 'error_file_size', 'error_validation', 'ready', 'started', 'getting_upload_params', 'error_upload_params', 'uploading', 'exception_upload', 'aborted', 'restarted', 'removed', 'error_upload', 'headers_received', 'done'__ | +| getUploadParams | func | | called after file is prepared and validated, right before upload; receives __fileWithMeta__ object; should return params needed for upload: __{ fields (object), headers (object), meta (object), method (string), url (string), withCredentials(bool) }__; omit to remove upload functionality from dropzone | +| onSubmit | func | | called when user presses submit button; receives array of __fileWithMeta__ objects whose status is __'headers_received'__ or __'done'__; also receives array of all __fileWithMeta__ objects as second argument; omit to remove submit button | +| accept | string | `'*'` | the accept attribute of the file dropzone/input | +| multiple | bool | `true` | the multiple attribute of the file input | +| minSizeBytes | number | `0` | min file size in bytes (1024 * 1024 = 1MB) | +| maxSizeBytes | number | `2^53 - 1` | max file size in bytes (1024 * 1024 = 1MB) | +| maxFiles | number | `2^53 - 1` | max number of files that can be tracked and rendered by the dropzone | +| validate | func | | generic validation function called after file is prepared; receives __fileWithMeta__ object; should return falsy value if validation succeeds; should return truthy value if validation fails, which sets __meta.status__ to __'error_validation'__, and sets __meta.validationError__ to the returned value | +| autoUpload | bool | `true` | pass false to prevent file from being uploaded automatically; sets __meta.status__ to __'ready'__ (instead of __'getting_upload_params'__) after file is prepared and validated; you can call __fileWithMeta.restart__ whenever you want to initiate file upload | +| timeout | number | | pass an integer to make upload time out after this many ms; if upload times out, onChangeStatus is invoked with value __'exception_upload'__ | +| initialFiles | `File[]` | | add these files to dropzone, without any user interaction; if a new array of `initialFiles` is passed, they will also be added to dropzone; see here for an [example of creating and uploading a file from a data URL](https://react-dropzone-uploader.js.org/docs/examples#initial-file-from-data-url) | + + +## Component Customization Props +| Name | Type | Default Value | Description | +| --- | --- | --- | --- | +| disabled | bool/func | `false` | true to disable dropzone and input | +| canCancel | bool/func | `true` | false to remove cancel button in file preview | +| canRestart | bool/func | `true` | false to remove restart button in file preview | +| canRemove | bool/func | `true` | false to remove remove button in file preview | +| inputContent | node/func | `'Drag Files or Click to Browse'` | child of input __label__; '' or null to remove | +| inputWithFilesContent | node/func | `'Add Files'` | child of input __label__; '' or null to remove | +| submitButtonDisabled | bool/func | `false` | true to disable submit button | +| submitButtonContent | node/func | `'Submit'` | '' or null to remove | +| classNames | object | `{}` | see "Custom Styles" section | +| styles | object | `{}` | see "Custom Styles" section | +| addClassNames | object | `{}` | see "Custom Styles" section | + + +## Component Injection Props +| Name | Type | Default Value | Description | +| --- | --- | --- | --- | +| InputComponent | func | | overrides __Input__; null to remove | +| PreviewComponent | func | | overrides __Preview__; null to remove | +| SubmitButtonComponent | func | | overrides __SubmitButton__; null to remove | +| LayoutComponent | func | | overrides __Layout__; can't be removed | + + +## Props Passed To Injected Components +If you use the component injection API, you'll want to know which props are passed to your injected components. Scroll to the bottom of the following files to see their prop types. + +- [InputComponent](https://github.com/fortana-co/react-dropzone-uploader/blob/master/src/Input.js) +- [PreviewComponent](https://github.com/fortana-co/react-dropzone-uploader/blob/master/src/Preview.js) +- [SubmitButtonComponent](https://github.com/fortana-co/react-dropzone-uploader/blob/master/src/SubmitButton.js) +- [LayoutComponent](https://github.com/fortana-co/react-dropzone-uploader/blob/master/src/Layout.js) diff --git a/docs/quick-start.md b/docs/quick-start.md index 97f7e4f..1bf43a8 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -1,34 +1,34 @@ ---- -id: quick-start -title: Quick Start ---- - - -React Dropzone Uploader is a customizable file dropzone and uploader for React. - - -## Installation -`npm install --save react-dropzone-uploader` - -Import default styles in your app. - -~~~js -import 'react-dropzone-uploader/dist/styles.css' -~~~ - - -## `Dropzone` -RDU handles common use cases with almost no config. The following code gives you a dropzone and clickable file input that accepts image, audio and video files. It uploads files to `https://httpbin.org/post`, and renders a button to submit files that are done uploading. - ->You can edit code for this example __and see changes live__. Open your browser's console to see how RDU manages file metadata and the upload lifecycle. - -
- - -## Browser Support -| Chrome | Firefox | Edge | Safari | IE | iOS Safari | Chrome for Android | -| --- | --- | --- | --- | --- | --- | --- | -| ✔ | ✔ | ✔ | 10+, 9\* | 11\* | ✔ | ✔ | - -\* requires `Promise` polyfill, e.g. [@babel/polyfill](https://babeljs.io/docs/en/babel-polyfill) - +--- +id: quick-start +title: Quick Start +--- + + +React Dropzone Uploader is a customizable file dropzone and uploader for React. + + +## Installation +`npm install --save react-dropzone-uploader` + +Import default styles in your app. + +~~~js +import 'react-dropzone-uploader/dist/styles.css' +~~~ + + +## `Dropzone` +RDU handles common use cases with almost no config. The following code gives you a dropzone and clickable file input that accepts image, audio and video files. It uploads files to `https://httpbin.org/post`, and renders a button to submit files that are done uploading. + +>You can edit code for this example __and see changes live__. Open your browser's console to see how RDU manages file metadata and the upload lifecycle. + +
+ + +## Browser Support +| Chrome | Firefox | Edge | Safari | IE | iOS Safari | Chrome for Android | +| --- | --- | --- | --- | --- | --- | --- | +| ✔ | ✔ | ✔ | 10+, 9\* | 11\* | ✔ | ✔ | + +\* requires `Promise` polyfill, e.g. [@babel/polyfill](https://babeljs.io/docs/en/babel-polyfill) + diff --git a/docs/s3.md b/docs/s3.md index 7f37626..b8960b4 100644 --- a/docs/s3.md +++ b/docs/s3.md @@ -1,51 +1,51 @@ ---- -id: s3 -title: S3 Uploader ---- - - -Let's say you want to upload a file to one of your S3 buckets, using __POST__. - -Your API has a protected endpoint that returns the necessary S3 upload params. Maybe it [uses Boto](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.Client.generate_presigned_post) to generate a [presigned upload URL](https://docs.aws.amazon.com/AmazonS3/latest/dev/PresignedUrlUploadObject.html). - -A successful request to this endpoint returns something like this: - -~~~json -{ - "fields": { - "AWSAccessKeyId": "AKIAJSQUO7ORWYVCSV6Q", - "acl": "public-read", - "key": "files/89789486-d94a-4251-a42d-18af752ab7d2-test.txt", - "policy": "eyJleHBpcmF0aW9uIjogIjIwMTgtMTAtMzBUMjM6MTk6NDdaIiwgImNvbmRpdGlvbnMiOiBbeyJhY2wiOiAicHVibGljLXJlYWQifSwgWyJjb250ZW50LWxlbmd0aC1yYW5nZSIsIDEwLCAzMTQ1NzI4MF0sIHsiYnVja2V0IjogImJlYW10ZWNoLWZpbGUifSwgeyJrZXkiOiAiY29tcGFueS8zLzg5Nzg5NDg2LWQ5NGEtNDI1MS1hNDJkLTE4YWY3NTJhYjdkMi10ZXN0LnR4dCJ9XX0=", - "signature": "L7r3KBtyOXjUKy31g42JTYb1sio=" - }, - "fileUrl": "https://my-bucket.s3.amazonaws.com/files/89789486-d94a-4251-a42d-18af752ab7d2-test.txt", - "uploadUrl": "https://my-bucket.s3.amazonaws.com/" -} -~~~ - -`fields` has everything you need to authenticate with your S3 bucket, but you need to add them to the request sent by RDU. It turns out this is super easy. - -~~~js -const getUploadParams = async ({ meta: { name } }) => { - const { fields, uploadUrl, fileUrl } = await myApiService.getPresignedUploadParams(name) - return { fields, meta: { fileUrl }, url: uploadUrl } -} -~~~ - -That's it. If `myApiService.getPresignedUploadParams` succeeds, you return `uploadUrl` as `url`. You also decide to merge `fileUrl` into your file's meta so you can use it later. RDU takes care of the rest, including appending the fields to the `FormData` instance used in the `XMLHttpRequest`. - -Let's say `myApiService.getPresignedUploadParams` fails and returns `{}`. In this case `uploadUrl` and hence `url` are undefined. RDU abandons the upload and changes the file's status to `'error_upload_params'`. At this point you might show the user an error message, and the user might remove the file or restart the upload. - - -## S3 using PUT instead of POST -Uploading a file to S3 using __PUT__ works differently than using __POST__. - -Basically, if you use PUT, you can't wrap your file in a `FormData` instance. In this case, `body` must be set to `file`, and the `fields` in the POST example are all encoded in the query string of the `uploadUrl`. `getUploadParams` would look a little different: - -~~~js -const getUploadParams = async ({ file, meta: { name } }) => { - const { uploadUrl, fileUrl } = await myApiService.getPresignedUploadParams(name) - return { body: file, meta: { fileUrl }, url: uploadUrl } -} -~~~ +--- +id: s3 +title: S3 Uploader +--- + + +Let's say you want to upload a file to one of your S3 buckets, using __POST__. + +Your API has a protected endpoint that returns the necessary S3 upload params. Maybe it [uses Boto](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.Client.generate_presigned_post) to generate a [presigned upload URL](https://docs.aws.amazon.com/AmazonS3/latest/dev/PresignedUrlUploadObject.html). + +A successful request to this endpoint returns something like this: + +~~~json +{ + "fields": { + "AWSAccessKeyId": "AKIAJSQUO7ORWYVCSV6Q", + "acl": "public-read", + "key": "files/89789486-d94a-4251-a42d-18af752ab7d2-test.txt", + "policy": "eyJleHBpcmF0aW9uIjogIjIwMTgtMTAtMzBUMjM6MTk6NDdaIiwgImNvbmRpdGlvbnMiOiBbeyJhY2wiOiAicHVibGljLXJlYWQifSwgWyJjb250ZW50LWxlbmd0aC1yYW5nZSIsIDEwLCAzMTQ1NzI4MF0sIHsiYnVja2V0IjogImJlYW10ZWNoLWZpbGUifSwgeyJrZXkiOiAiY29tcGFueS8zLzg5Nzg5NDg2LWQ5NGEtNDI1MS1hNDJkLTE4YWY3NTJhYjdkMi10ZXN0LnR4dCJ9XX0=", + "signature": "L7r3KBtyOXjUKy31g42JTYb1sio=" + }, + "fileUrl": "https://my-bucket.s3.amazonaws.com/files/89789486-d94a-4251-a42d-18af752ab7d2-test.txt", + "uploadUrl": "https://my-bucket.s3.amazonaws.com/" +} +~~~ + +`fields` has everything you need to authenticate with your S3 bucket, but you need to add them to the request sent by RDU. It turns out this is super easy. + +~~~js +const getUploadParams = async ({ meta: { name } }) => { + const { fields, uploadUrl, fileUrl } = await myApiService.getPresignedUploadParams(name) + return { fields, meta: { fileUrl }, url: uploadUrl } +} +~~~ + +That's it. If `myApiService.getPresignedUploadParams` succeeds, you return `uploadUrl` as `url`. You also decide to merge `fileUrl` into your file's meta so you can use it later. RDU takes care of the rest, including appending the fields to the `FormData` instance used in the `XMLHttpRequest`. + +Let's say `myApiService.getPresignedUploadParams` fails and returns `{}`. In this case `uploadUrl` and hence `url` are undefined. RDU abandons the upload and changes the file's status to `'error_upload_params'`. At this point you might show the user an error message, and the user might remove the file or restart the upload. + + +## S3 using PUT instead of POST +Uploading a file to S3 using __PUT__ works differently than using __POST__. + +Basically, if you use PUT, you can't wrap your file in a `FormData` instance. In this case, `body` must be set to `file`, and the `fields` in the POST example are all encoded in the query string of the `uploadUrl`. `getUploadParams` would look a little different: + +~~~js +const getUploadParams = async ({ file, meta: { name } }) => { + const { uploadUrl, fileUrl } = await myApiService.getPresignedUploadParams(name) + return { body: file, meta: { fileUrl }, url: uploadUrl } +} +~~~ diff --git a/docs/typescript.md b/docs/typescript.md index 79d2f07..0535a51 100644 --- a/docs/typescript.md +++ b/docs/typescript.md @@ -1,51 +1,51 @@ ---- -id: typescript -title: TypeScript ---- - - -RDU [ships with precise TypeScript definitions](https://github.com/fortana-co/react-dropzone-uploader/blob/master/src/Dropzone.d.ts) for just about everything in the library. - -RDU has a flexible and powerful API. Using TypeScript will help you use it properly, and help you get a handle on everything it can do. - -It makes it especially easy to see and inspect the props passed to injected components, and the arguments passed to function props. 🚀 - -~~~js -import Dropzone, { IDropzoneProps, ILayoutProps } from 'react-dropzone-uploader' - -// add type defs to custom LayoutComponent prop to easily inspect props passed to injected components -const Layout = ({ input, previews, submitButton, dropzoneProps, files, extra: { maxFiles } }: ILayoutProps) => { - return ( -
- {previews} - -
{files.length < maxFiles && input}
- - {files.length > 0 && submitButton} -
- ) -} - -const CustomLayout = () => { - // add type defs to function props to get TS support inside function bodies, - // and not just where functions are passed as props into Dropzone - const getUploadParams: IDropzoneProps['getUploadParams'] = () => ({ url: 'https://httpbin.org/post' }) - - const handleSubmit: IDropzoneProps['onSubmit'] = (files, allFiles) => { - console.log(files.map(f => f.meta)) - allFiles.forEach(f => f.remove()) - } - - return ( - - ) -} -~~~ - -If you want more examples, [check out the ones used to test RDU's TypeScript definitions](https://github.com/fortana-co/react-dropzone-uploader/blob/master/examples/src/index.tsx). +--- +id: typescript +title: TypeScript +--- + + +RDU [ships with precise TypeScript definitions](https://github.com/fortana-co/react-dropzone-uploader/blob/master/src/Dropzone.d.ts) for just about everything in the library. + +RDU has a flexible and powerful API. Using TypeScript will help you use it properly, and help you get a handle on everything it can do. + +It makes it especially easy to see and inspect the props passed to injected components, and the arguments passed to function props. 🚀 + +~~~js +import Dropzone, { IDropzoneProps, ILayoutProps } from 'react-dropzone-uploader' + +// add type defs to custom LayoutComponent prop to easily inspect props passed to injected components +const Layout = ({ input, previews, submitButton, dropzoneProps, files, extra: { maxFiles } }: ILayoutProps) => { + return ( +
+ {previews} + +
{files.length < maxFiles && input}
+ + {files.length > 0 && submitButton} +
+ ) +} + +const CustomLayout = () => { + // add type defs to function props to get TS support inside function bodies, + // and not just where functions are passed as props into Dropzone + const getUploadParams: IDropzoneProps['getUploadParams'] = () => ({ url: 'https://httpbin.org/post' }) + + const handleSubmit: IDropzoneProps['onSubmit'] = (files, allFiles) => { + console.log(files.map(f => f.meta)) + allFiles.forEach(f => f.remove()) + } + + return ( + + ) +} +~~~ + +If you want more examples, [check out the ones used to test RDU's TypeScript definitions](https://github.com/fortana-co/react-dropzone-uploader/blob/master/examples/src/index.tsx). diff --git a/docs/why-rdu.md b/docs/why-rdu.md index 772a21d..4db017a 100644 --- a/docs/why-rdu.md +++ b/docs/why-rdu.md @@ -1,176 +1,176 @@ ---- -id: why-rdu -title: Why RDU? ---- - - -React already has dropzone/uploader libraries, two of the most popular being [react-fine-uploader](https://github.com/FineUploader/react-fine-uploader) and [React-Dropzone-Component](https://github.com/felixrieseberg/React-Dropzone-Component). Why did I write this one? - -1. __The others weren't really built for React__. They're wrappers around [fine-uploader](https://fineuploader.com/) and [Dropzone.js](https://www.dropzonejs.com/), file uploaders with sprawling APIs that weren't designed with React in mind. Both weigh many times more than RDU. -2. __They're not maintained__. The react-fine-uploader and fine-uploader repos were shut down in 2018. React-Dropzone-Component hasn't seen a commit to source code since 2017; [pretty much ditto](https://gitlab.com/meno/dropzone/issues/74) with Dropzone.js. - -My goal with RDU was to build a lightweight, customizable dropzone and uploader with a minimal API, sensible defaults, and great support for TypeScript. __It tries to make the easy things effortless, and the hardest things possible__. - - -## React Dropzone -There's also the popular and solid [react-dropzone](https://react-dropzone.netlify.com/), but this library only gives you a dropzone — it has no API for managing uploads. - -File uploads with status, progress, cancellation and restart are hard to get right. And they're the most common use case for a dropzone, so I thought it would be nice to build a library that gives you a dropzone AND handles file uploads. - -I also wanted a friendlier rendering API and better rendering defaults. __react-dropzone doesn't help you with rendering__: - -1. __It doesn't provide file previews__. You have to write them yourself, [which means lots of boilerplate for even basic previews](https://react-dropzone.netlify.com/#previews). -2. __It doesn't provide default styles__. This makes no difference if you style everything yourself, but I wanted a component, like [React Select](https://react-select.com/styles), that looks good and works well out of the box, and makes it trivial to override individual styles. -3. __It actually renders nothing by default__. To render anything you [have to pass a render prop](https://react-dropzone.netlify.com/) as a child of `Dropzone`. This means understanding `getRootProps`, `getInputProps`, and `isDragActive`, and writing 10+ lines of boilerplate. - -RDU abstracts away things like `getRootProps` and `getInputProps`, which for most cases are implementation details. Of course it lets you access them and take full control of rendering using the component injection API if you want to. - - -## RDU vs React Dropzone -Here's a comparison of RDU and React Dropzone for implementing a dropzone that uploads files to : - -__react-dropzone-uploader__: uploads files, and removes them if upload is successful. - -~~~js -const DropzoneWithPreview = () => { - return ( - ({ url: 'https://httpbin.org/post' })} - onChangeStatus={({ remove }, status) => { if (status === 'headers_received') remove() }} - accept="image/*,audio/*,video/*" - /> - ) -} -~~~ - -__react-dropzone__ (code mostly taken from React Dropzone's docs): - -Uploads files, and removes them if upload is successful. Doesn't handle upload failure. Previews have no upload progress. No active state on drag over. No reject state if dragged files have incorrect file types. Behaves incorrectly if user drags a second group of files before first group has finished uploading (bonus points if you can spot why this happens). - -~~~js -const thumbsContainer = { - display: 'flex', - flexDirection: 'row', - flexWrap: 'wrap', - marginTop: 16 -} -​ -const thumb = { - display: 'inline-flex', - borderRadius: 2, - border: '1px solid #eaeaea', - marginBottom: 8, - marginRight: 8, - width: 100, - height: 100, - padding: 4, - boxSizing: 'border-box' -} -​ -const thumbInner = { - display: 'flex', - minWidth: 0, - overflow: 'hidden' -} -​ -const img = { - display: 'block', - width: 'auto', - height: '100%' -} -​ -class DropzoneWithPreview extends React.Component { - constructor() { - super() - this.state = { - files: [] - } - } -​ - onDrop(files) { - this.setState({ - files: files.map(file => Object.assign(file, { - preview: URL.createObjectURL(file) - })) - }) - - const uploaders = files.map(file => { - const formData = new FormData() - formData.append('file', file); - - return axios.post('https://httpbin.org/post', formData, { - headers: { 'X-Requested-With': 'XMLHttpRequest' }, - }) - }) - - axios.all(uploaders).then(() => { - // remove files once they've all been uploaded - this.setState({ files: [] }) - }) - } -​ - componentWillUnmount() { - // Make sure to revoke the data uris to avoid memory leaks - this.state.files.forEach(file => URL.revokeObjectURL(file.preview)) - } -​ - render() { - const {files} = this.state -​ - const thumbs = files.map(file => ( -
-
- -
-
- )) -​ - return ( -
- - {({getRootProps, getInputProps}) => ( -
- -

Drop files here

-
- )} -
- -
- ) - } -} -~~~ - -10 lines of code that are production ready, vs 100 that aren't even close. - - -## Contributing -If you like RDU and want to make it better, read on! - -Possible improvements to RDU are tracked as [GitHub issues with the "__help wanted__" tag](https://github.com/fortana-co/react-dropzone-uploader/labels/help%20wanted). - -For example, I don't know much about writing tests for React components, and I know RDU would benefit from having them. I'd also like better support for older browsers. - -I'd be super happy to give contributors complete control of implementing these features. - - -### Running Dev -Clone the project, install dependencies, and run the dev server. - -~~~sh -git clone git://github.com/fortana-co/react-dropzone-uploader.git -cd react-dropzone-uploader -npm install # or `yarn install` -npm run dev -~~~ - -This runs code in `examples/src/index.js`, which has many examples that use `Dropzone`. The library source code is in the `/src` directory. +--- +id: why-rdu +title: Why RDU? +--- + + +React already has dropzone/uploader libraries, two of the most popular being [react-fine-uploader](https://github.com/FineUploader/react-fine-uploader) and [React-Dropzone-Component](https://github.com/felixrieseberg/React-Dropzone-Component). Why did I write this one? + +1. __The others weren't really built for React__. They're wrappers around [fine-uploader](https://fineuploader.com/) and [Dropzone.js](https://www.dropzonejs.com/), file uploaders with sprawling APIs that weren't designed with React in mind. Both weigh many times more than RDU. +2. __They're not maintained__. The react-fine-uploader and fine-uploader repos were shut down in 2018. React-Dropzone-Component hasn't seen a commit to source code since 2017; [pretty much ditto](https://gitlab.com/meno/dropzone/issues/74) with Dropzone.js. + +My goal with RDU was to build a lightweight, customizable dropzone and uploader with a minimal API, sensible defaults, and great support for TypeScript. __It tries to make the easy things effortless, and the hardest things possible__. + + +## React Dropzone +There's also the popular and solid [react-dropzone](https://react-dropzone.netlify.com/), but this library only gives you a dropzone — it has no API for managing uploads. + +File uploads with status, progress, cancellation and restart are hard to get right. And they're the most common use case for a dropzone, so I thought it would be nice to build a library that gives you a dropzone AND handles file uploads. + +I also wanted a friendlier rendering API and better rendering defaults. __react-dropzone doesn't help you with rendering__: + +1. __It doesn't provide file previews__. You have to write them yourself, [which means lots of boilerplate for even basic previews](https://react-dropzone.netlify.com/#previews). +2. __It doesn't provide default styles__. This makes no difference if you style everything yourself, but I wanted a component, like [React Select](https://react-select.com/styles), that looks good and works well out of the box, and makes it trivial to override individual styles. +3. __It actually renders nothing by default__. To render anything you [have to pass a render prop](https://react-dropzone.netlify.com/) as a child of `Dropzone`. This means understanding `getRootProps`, `getInputProps`, and `isDragActive`, and writing 10+ lines of boilerplate. + +RDU abstracts away things like `getRootProps` and `getInputProps`, which for most cases are implementation details. Of course it lets you access them and take full control of rendering using the component injection API if you want to. + + +## RDU vs React Dropzone +Here's a comparison of RDU and React Dropzone for implementing a dropzone that uploads files to : + +__react-dropzone-uploader__: uploads files, and removes them if upload is successful. + +~~~js +const DropzoneWithPreview = () => { + return ( + ({ url: 'https://httpbin.org/post' })} + onChangeStatus={({ remove }, status) => { if (status === 'headers_received') remove() }} + accept="image/*,audio/*,video/*" + /> + ) +} +~~~ + +__react-dropzone__ (code mostly taken from React Dropzone's docs): + +Uploads files, and removes them if upload is successful. Doesn't handle upload failure. Previews have no upload progress. No active state on drag over. No reject state if dragged files have incorrect file types. Behaves incorrectly if user drags a second group of files before first group has finished uploading (bonus points if you can spot why this happens). + +~~~js +const thumbsContainer = { + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap', + marginTop: 16 +} +​ +const thumb = { + display: 'inline-flex', + borderRadius: 2, + border: '1px solid #eaeaea', + marginBottom: 8, + marginRight: 8, + width: 100, + height: 100, + padding: 4, + boxSizing: 'border-box' +} +​ +const thumbInner = { + display: 'flex', + minWidth: 0, + overflow: 'hidden' +} +​ +const img = { + display: 'block', + width: 'auto', + height: '100%' +} +​ +class DropzoneWithPreview extends React.Component { + constructor() { + super() + this.state = { + files: [] + } + } +​ + onDrop(files) { + this.setState({ + files: files.map(file => Object.assign(file, { + preview: URL.createObjectURL(file) + })) + }) + + const uploaders = files.map(file => { + const formData = new FormData() + formData.append('file', file); + + return axios.post('https://httpbin.org/post', formData, { + headers: { 'X-Requested-With': 'XMLHttpRequest' }, + }) + }) + + axios.all(uploaders).then(() => { + // remove files once they've all been uploaded + this.setState({ files: [] }) + }) + } +​ + componentWillUnmount() { + // Make sure to revoke the data uris to avoid memory leaks + this.state.files.forEach(file => URL.revokeObjectURL(file.preview)) + } +​ + render() { + const {files} = this.state +​ + const thumbs = files.map(file => ( +
+
+ +
+
+ )) +​ + return ( +
+ + {({getRootProps, getInputProps}) => ( +
+ +

Drop files here

+
+ )} +
+ +
+ ) + } +} +~~~ + +10 lines of code that are production ready, vs 100 that aren't even close. + + +## Contributing +If you like RDU and want to make it better, read on! + +Possible improvements to RDU are tracked as [GitHub issues with the "__help wanted__" tag](https://github.com/fortana-co/react-dropzone-uploader/labels/help%20wanted). + +For example, I don't know much about writing tests for React components, and I know RDU would benefit from having them. I'd also like better support for older browsers. + +I'd be super happy to give contributors complete control of implementing these features. + + +### Running Dev +Clone the project, install dependencies, and run the dev server. + +~~~sh +git clone git://github.com/fortana-co/react-dropzone-uploader.git +cd react-dropzone-uploader +npm install # or `yarn install` +npm run dev +~~~ + +This runs code in `examples/src/index.js`, which has many examples that use `Dropzone`. The library source code is in the `/src` directory. diff --git a/examples/Accept.md b/examples/Accept.md index f1615a9..03f3711 100644 --- a/examples/Accept.md +++ b/examples/Accept.md @@ -1,39 +1,39 @@ -Only accepts __image__, __audio__, and __video__ files. Colors dropzone red on drag if files will be rejected because of file type. - -Customization functions that receive `(files, extra)` allow `inputContent` and `inputLabel` style to react to dropzone state. - -Also merges extra `fileUrl` field into file meta. - -~~~js -const ImageAudioVideo = () => { - const getUploadParams = ({ meta }) => { - const url = 'https://httpbin.org/post' - return { url, meta: { fileUrl: `${url}/${encodeURIComponent(meta.name)}` } } - } - - const handleChangeStatus = ({ meta }, status) => { - console.log(status, meta) - } - - const handleSubmit = (files, allFiles) => { - console.log(files.map(f => f.meta)) - allFiles.forEach(f => f.remove()) - } - - return ( - (extra.reject ? 'Image, audio and video files only' : 'Drag Files')} - styles={{ - dropzoneReject: { borderColor: 'red', backgroundColor: '#DAA' }, - inputLabel: (files, extra) => (extra.reject ? { color: 'red' } : {}), - }} - /> - ) -} - - -~~~ +Only accepts __image__, __audio__, and __video__ files. Colors dropzone red on drag if files will be rejected because of file type. + +Customization functions that receive `(files, extra)` allow `inputContent` and `inputLabel` style to react to dropzone state. + +Also merges extra `fileUrl` field into file meta. + +~~~js +const ImageAudioVideo = () => { + const getUploadParams = ({ meta }) => { + const url = 'https://httpbin.org/post' + return { url, meta: { fileUrl: `${url}/${encodeURIComponent(meta.name)}` } } + } + + const handleChangeStatus = ({ meta }, status) => { + console.log(status, meta) + } + + const handleSubmit = (files, allFiles) => { + console.log(files.map(f => f.meta)) + allFiles.forEach(f => f.remove()) + } + + return ( + (extra.reject ? 'Image, audio and video files only' : 'Drag Files')} + styles={{ + dropzoneReject: { borderColor: 'red', backgroundColor: '#DAA' }, + inputLabel: (files, extra) => (extra.reject ? { color: 'red' } : {}), + }} + /> + ) +} + + +~~~ diff --git a/examples/CustomInput.md b/examples/CustomInput.md index fab5318..7543452 100644 --- a/examples/CustomInput.md +++ b/examples/CustomInput.md @@ -1,53 +1,53 @@ -Standard file uploader with custom `InputComponent`. Passes custom `getFilesFromEvent` prop, from [html5-file-selector](https://github.com/quarklemotion/html5-file-selector) library, to allow recursive folder drag and drop. - -~~~js -// import { getDroppedOrSelectedFiles } from 'html5-file-selector' - -const Input = ({ accept, onFiles, files, getFilesFromEvent }) => { - const text = files.length > 0 ? 'Add more files' : 'Choose files' - - return ( - - ) -} - -const CustomInput = () => { - const handleSubmit = (files, allFiles) => { - console.log(files.map(f => f.meta)) - allFiles.forEach(f => f.remove()) - } - - const getFilesFromEvent = e => { - return new Promise(resolve => { - getDroppedOrSelectedFiles(e).then(chosenFiles => { - resolve(chosenFiles.map(f => f.fileObject)) - }) - }) - } - - return ( - ({ url: 'https://httpbin.org/post' })} - onSubmit={handleSubmit} - InputComponent={Input} - getFilesFromEvent={getFilesFromEvent} - /> - ) -} - - -~~~ +Standard file uploader with custom `InputComponent`. Passes custom `getFilesFromEvent` prop, from [html5-file-selector](https://github.com/quarklemotion/html5-file-selector) library, to allow recursive folder drag and drop. + +~~~js +// import { getDroppedOrSelectedFiles } from 'html5-file-selector' + +const Input = ({ accept, onFiles, files, getFilesFromEvent }) => { + const text = files.length > 0 ? 'Add more files' : 'Choose files' + + return ( + + ) +} + +const CustomInput = () => { + const handleSubmit = (files, allFiles) => { + console.log(files.map(f => f.meta)) + allFiles.forEach(f => f.remove()) + } + + const getFilesFromEvent = e => { + return new Promise(resolve => { + getDroppedOrSelectedFiles(e).then(chosenFiles => { + resolve(chosenFiles.map(f => f.fileObject)) + }) + }) + } + + return ( + ({ url: 'https://httpbin.org/post' })} + onSubmit={handleSubmit} + InputComponent={Input} + getFilesFromEvent={getFilesFromEvent} + /> + ) +} + + +~~~ diff --git a/examples/CustomLayout.md b/examples/CustomLayout.md index 27f1d65..0af1c95 100644 --- a/examples/CustomLayout.md +++ b/examples/CustomLayout.md @@ -1,38 +1,38 @@ -Custom `LayoutComponent`. Renders file previews above dropzone, and submit button below it. Uses `defaultClassNames` and `classNames` prop to ensure input label style doesn't change when dropzone contains files. - -~~~js -const Layout = ({ input, previews, submitButton, dropzoneProps, files, extra: { maxFiles } }) => { - return ( -
- {previews} - -
- {files.length < maxFiles && input} -
- - {files.length > 0 && submitButton} -
- ) -} - -const CustomLayout = () => { - const getUploadParams = () => ({ url: 'https://httpbin.org/post' }) - - const handleSubmit = (files, allFiles) => { - console.log(files.map(f => f.meta)) - allFiles.forEach(f => f.remove()) - } - - return ( - - ) -} - - -~~~ +Custom `LayoutComponent`. Renders file previews above dropzone, and submit button below it. Uses `defaultClassNames` and `classNames` prop to ensure input label style doesn't change when dropzone contains files. + +~~~js +const Layout = ({ input, previews, submitButton, dropzoneProps, files, extra: { maxFiles } }) => { + return ( +
+ {previews} + +
+ {files.length < maxFiles && input} +
+ + {files.length > 0 && submitButton} +
+ ) +} + +const CustomLayout = () => { + const getUploadParams = () => ({ url: 'https://httpbin.org/post' }) + + const handleSubmit = (files, allFiles) => { + console.log(files.map(f => f.meta)) + allFiles.forEach(f => f.remove()) + } + + return ( + + ) +} + + +~~~ diff --git a/examples/CustomPreview.md b/examples/CustomPreview.md index 18cbcef..ac4471c 100644 --- a/examples/CustomPreview.md +++ b/examples/CustomPreview.md @@ -1,33 +1,33 @@ -Standard file uploader with custom `PreviewComponent`. Also disables dropzone while files are being uploaded. - -~~~js -const Preview = ({ meta }) => { - const { name, percent, status } = meta - return ( - - {name}, {Math.round(percent)}%, {status} - - ) -} - -const CustomPreview = () => { - const getUploadParams = () => ({ url: 'https://httpbin.org/post' }) - - const handleSubmit = (files, allFiles) => { - console.log(files.map(f => f.meta)) - allFiles.forEach(f => f.remove()) - } - - return ( - files.some(f => ['preparing', 'getting_upload_params', 'uploading'].includes(f.meta.status))} - /> - ) -} - - -~~~ +Standard file uploader with custom `PreviewComponent`. Also disables dropzone while files are being uploaded. + +~~~js +const Preview = ({ meta }) => { + const { name, percent, status } = meta + return ( + + {name}, {Math.round(percent)}%, {status} + + ) +} + +const CustomPreview = () => { + const getUploadParams = () => ({ url: 'https://httpbin.org/post' }) + + const handleSubmit = (files, allFiles) => { + console.log(files.map(f => f.meta)) + allFiles.forEach(f => f.remove()) + } + + return ( + files.some(f => ['preparing', 'getting_upload_params', 'uploading'].includes(f.meta.status))} + /> + ) +} + + +~~~ diff --git a/examples/InitialFileFromDataUrl.md b/examples/InitialFileFromDataUrl.md index 6713ca9..dd874e9 100644 --- a/examples/InitialFileFromDataUrl.md +++ b/examples/InitialFileFromDataUrl.md @@ -1,58 +1,58 @@ -User can choose files with input, but can't drop them. - -~~~js -const imageDataUrl = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxISEhUTEhIVFRUVFRUVEBcVFRUPFRUSFRUWFhUVFRUYHSggGBolGxUVITEhJSkrLi4uFx8zODMtNygtLisBCgoKDg0OGxAQGy0mICUtLS0tLS8tLS0uLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLf/AABEIALcBFAMBIgACEQEDEQH/xAAbAAACAwEBAQAAAAAAAAAAAAAEBQACAwEGB//EADkQAAEDAgQEAwcEAgEEAwAAAAEAAhEDIQQSMUEFUWFxIoGRBhMyobHR8BRCweFS8YIjYpLSFTNy/8QAGgEAAwEBAQEAAAAAAAAAAAAAAgMEAQUABv/EAC0RAAICAgICAAQFBAMAAAAAAAABAhEDIRIxBEETIlFhFCMygZFCcfDxBaHR/9oADAMBAAIRAxEAPwDwnDA5PGtMILhtJOW07L6fHGkfLeRkuQFncoSUa2itBh0YjmhQ/Mq5SU4dhlkcOsoJZULBSXXUkxFBd/TLOJvxRFWpELJgTuthkC/CQbIXAfDKmiUaSJFJcoBFQjSFTk7BvdKCjC3KpmWg8mUM81g+mUWFHNXmaptF8HXA1RxxghKXU1wUys5HnsMqcR5BQcQQ/uVQ0l6zPlLV+Ik6IMOcTKKdRAaXHYSvL43ir3nIWhhZOaDZ2kG/lA6+i5ZEmkyvx8Ly/p69npHcSY0eIgnkL/PZA1eNTowX0mTz5dkiBJBtpBG0A/Wcw+SuRr017j+ymLZfDwsUe1Y0pcRaToAOmu23mneEcHNkGQdF41/SADtOn9Xt2RWCxr6RlpIEtkHQyATbnvqsWgM/hqS+TTPXCmrFqx4dj21m5hY/uHI/yEU5wCZRx5RlGVMX4mmqYdqIruCyZZYxieqNKt0vr00cXrF4WPZsG0KX4dZMw10zc1VaxL4IpWV0B/p1EdAUW8EZ8RhXDwnFM2SGlVyotuNCJdEeWDk7GgK2YldHEymNFyInnFo0IVci0C6AhF2Ztpq4pLRoWgC0xsEqUELUohMqhSzG1IWjINtgdUALL36GxWJQgroXKi6OJtbGTqyjHSloqSmGDC8nZ6cOKDKdNa+7VjUaxpc4w0CSTyXm8Z7RPc7weFm3M9T9gtStgYcGTM/l/kfupqzGLzQxjna/n5KJpYoiL33Ou+6b8D7lb/4+Vfq/6Hxak2N421pIY3NFpmBPTmheJcTe8ZQYFwY1JEa9OiVtYJIPlvfafn6oeFdjPH8Ct5N/YZVOLVXgsIYAcoMB0w6/0SJ7c1VxPOCT0gE/JMMkCZBtJAGhMgDp/aGZhra33QPHza10dDHjjjVRVF6cmxJIcQHSYzCxGY+XlCu9hcM7iZPP90QNed12lTbpBcSCLCYOgsNf7RNHBmB7wgNbMNm9zf5wmqFBNgJi5i8b/wCWb52WZYY7nuNPrdF+7a3WSf8Ax1j7qDFxGVrRptyOhnUWXnFGneG1XscDBg67SP6ylOv1pKRsxTn2EkwfTV1l39W7Wdjre/M+aXJ+kyXN4yyO/Y69/KnvUto4wExodtpG3miQ5KtkE8Lg6YUHqpcqByhKKwKISqmpC44rCo5C2Go2XNRRCklRDyGcAhtVdlY0wtcq9ZjSQfgXp7hnWXm8O6CnWEqpsXoi8iPsatVgqUjIWi0hZ1q0BWJdC4aq8ZVna4SXiTTBTjPKFxLAVo3G+LPHuYZuqFpTnE4cShnUEhwOpHMmA0ymmDehxQW9OlCKKaBySUkJuLY19VxE+AfC0adz1QjG6m0DYHnayKxdHxuGhG2hvy5hDCxvtp3CrSS6OtijFQSj0E0GacomenM9eiIqiLCNcpvM7yDy0UoMIaCZ8QBjpm19ZCu1g0IE68jYRHKFrkGCtYCL6l2sjlcHeL/VatojqBFtPi3vujMPhp2A0F7kzyHmu1PciziT2t8l7jfZ6xdWMyG23I6rPD4YvMC3M/m/RMGOo7M+Z+ZWlKqzQMj9xudgTz/JRNa0esqKzKbYpi5BzOOp0jtBkoV1UvJnYHzAkieaJfhQRIdADS7xQJlwESInf0QtQC0TOXxcpk6fL1S7PGLaQOWXR/laY1v9B5LFjHG2WdRGmxJE+RKIeW6HpMXkzfdZO0nrf66IWmwiraRgG8ODgCLTA0vqJhZ0WZiADHchosJNzYK7XkGQSDpOh+XdUe0R1MRtEfg9EtwZ4o6tdpF4Df8AtuBEeG/nqi8HjgYa6ZgXjWYH869ENmIPh/a4uabE7ROx0+ZWFU21sNO9pAQSjQM8amqZ6RrVaFjwqoX05d8QOV3fY+iIqNWHHkmpOLB6pQbjKLeJXG0VlWMi0jFrFEa2ioi4A/EKNpwrBq4yqtgEKQLb9meVGYSpCwDVo1iJC5U1Q6oVUY16SUKkI5mIEJhFOGzfEVEvOJupia6EpiVjDhBVsZ0MQrVHoJjVoSvWY4qylVsrI0lu1bNpr1BcqAxRWgpIXi/F6dDw/E/XLy7leaqccruPxR0ytt2kLyorw+Jkyrl0j1j8I11y0EjQ2MJNxnChpaWgTOnPrCDw+Lqa+8Nxe8m1r+S0q1SS0tBJbcFx/gp8YPuyzF4s8c75aDqdxrcC40EC4kHa30VWVGSLzrAPoNr6IHFV5JDfh2E/ayq2jA2FvE46CDqCjpFpMQ5xcQCRDoE2iVKVAk3vYj6ifVVbiG6g6HXrOoG6s2tmGkAkamTfXy8liaZuxnTwtKxe7bxNb/2jWdpW/vsOyP8ApyCHbxPn3+iWVrOdlJjRsOBnTWALeS4ylYixJAOsQBMjvMIqB9B4r0CIyctzKEr4YExTecm9rzYkdRYaoYUD1F1ZtF3P87r1II2Zg6Q1JPnHorO4fROjnt53Dh077oV1Z/P7E9Vw1yNWiJiRbr9ljR4No8HpkgGqbxEMv5XQ+K4U5oljw8biIcJ6ctFZtQPEh0GdNNQeXYKnvS0Ah15Mi8tiMu3MayfLcGqPWLXDY2joQeyxrgWgzIBPR24TLEVQ74hJ/wAhY26IXiFANIgRYAzE5x8WmyVNaCGfss+Q9s2BaY0GY5pMdoTWtTS/2TpQyo7YuAHcC/1TCvVhJ9HF8l/nyr/NAdVsKtJyzxGICGZiLrFJWGoNoctUQzKtlEzkIcGLcHVlNqK89hXQmtPEWSYPRVnx70MQtA8JX+qV210xMneJhz6qwOKKEqViuUmkrOQSxJLYex5KOoMQ+FpJlSppiJskktIzVXlEOYhqwXhUXZxtRTG48UqZdqdGjm46JVi65alOPxbqhHILYtN0WYfF+JJX0AVpe4ucZcSS7uVxtNHYHAGoRYhp/dFjeLTrdeiwnDKNFpeRnNwC6csxYARa6PgkrO1yS0J6FNrAM4Obl0jwiOczvoFz3YIGsxLp0N9r/kK+KBMy2ZLjINyZ1IFhvtuuaaDa+94k/dHGVoyiZYuYHPRs/wAJVxKu52hytFtZnmT3Rr2k3N4ub2/LhKcQHPMDQaJWeTcaQcUE4cQANbR23n6+qMyk3JbYW0kjbzQQ4XXA8Pi6DXylD/pq2bKWuk7EJSnKGuLNtP2Nv1TGgEkb2Bk+iuzjAi1Mk6ySBZDUOFZR47Ryuf6R9JlNo+Hw2u48tOwVMVke3oF0CV+PVBJFJgjmXa84CzwntISYdSBHNpuI72KIxPGqDbCkHxuQCPLMh2Y2k6SxgaewBU9t5Kjl/bQXroaNrMeJFuiGxFG9roR+IPMqxrmYd8RAI1tNwAOohVuSBOGR9Ow1IHqtnvDhvIADr3LhMEjX06LlN7HSCSD3JkrF9KowzFhuDPrH8pUt9GkeYNvLcRMi646mCLnabbbfndVBtv8AbkpzBH0m2pnyQs8eg4HVaylkmS0mTzm8/wAeSD4piY0SqliC289/9eqG4hiSfNTZ3whaIvwn5rl9SV8YphsRJStz0TgzdcqOdymWSxJRPV4cy1RVwj/CFxdVPRyJLYquFz3xRdWmhxSuluLLVJM0oAlH06SphmI6m1OjEnyTM6eHRVGgtaTVsGo0iOeRmlFsIphQzFu1GSyN1hWYtmFWcJWAJ0ee4nQkFKcNgsxnUBxaQDB+Hw/nRem4gwBpJ0ASc4imDMENzS6CCR4bDMCJm9kzFHdnZ8GTlFhVKqKQiXAZfBbXpBNmyCsq+PDi5rzZxkRZpd05feEudirT8V7tM6DQ9PiSzFVjGv4U2cklbOgo2PWNN4sI8R8589Pkq1WF2aAC1kBxnSTqLwTAKxwTXCm2mCMzpmXBoG+U8tAsg8gGNcpiN+ebrBI8tLkpblrRoHjsSS/IAJM5v2gT9pTHDYOcpLdWgg/5cyet0rwlKahcXAEgzJIzE9R3HovV8Hw7cvipB27CYIGoP+xzK9hvbkZJm9PD2noABYgAdQB1SnG4kNcS219Qc0co9Uy4tVbTbcHObZpNmix00Ebc15itimtkmHOOgBuJ3PaPmmymkrZiRvisSGQ5xDiRMXkGTLTbskuNxL6vxaDQCwGy3ymo4mLk2HlzsEfguFvqBwY1pyxMkN1m0+RUmSM82rpDFURLQbmtuNOoVX0SLhMqvD3A5m2Ik3i0ahaNpZwLXIuL2PL85pC8X+iXfphchW3FndEUMXEnMfrN58tFTFYf880udIMKXNPLhdS2gkkx3h8VNied4vBABHUQPmjqOIIP18+m687RqQm1J3hknxTESBAIJmNdeSs8bNyQMlQVXpCMzIjUjcduiDJhGsqz4haZDo2JuJ6f+pQeLgG2huPPZUuSoFGb9PPz/wBX+SxiddN1A6V0pLpmi2o2CRyWuGfBVsay880O0wuDkj8LK0M7R6bDV/ColVHE2UV8cyohlg2OarlkCuucqAqwWloMouR1FyVU3ouhVRpickRxRKIa1C4R0o2UZzp9kaFs1UYtAFolmjVoFRoWrAsAZ572qxWXIwAEnxGeQ0/leZdWmLAQI73Jk+qZ+07HPxZYNcrco/4z90jaUalSPo/Cgo4Yr7X/ACbmqQIGoMg9fwBVwtPM7M7Rvzd+XVKzxPhPaRB6SicP8Gt50g3mTM+gjqvN2yoNsQAbOBzTM5g4CIbG3c721WOIaSc2g2AOhgNk31gD1V6NMNLgXSC0XbDvERIBM23nqNFvUptMAWAm5Mzy7HQI4Rt2wRZhqM1YE8wQC6GgGZA8l67APbTpZ3nK0CS42EA328oXm+FFn6jK7PdpgtAdeLAg7df9oP2l4gXRSafA3XbMdienJKy5fhY5NGU5Sovxbj5rv/6YjZpdBsOQ5oGnh3akG5N9ATvB31WXDcMXSRMi7SBm8W08hO69dR4c4O97VLGw2XGA1gBBG9vwKbx1LKueR/2+gyTUdIXYHBzHkYO8cvX5po3h8zEgX3+UrBnGKNOG02uqkA5f2tvykTe2y0qYTEYkeNzQIn3bTlgbZm6+qvjNf0i39wXHcQo0xDR713IHK3zd9khw9VzajnVARnuJMAHNzOosWr1dH2bHfTpyn+Uj9paTGkUWmXWc865RBhvfeOgUnlRdc72ul9w4tdGWKaJjp/P9JTiGeJM2Ee7bOoEG0WGnmhvd5pcszr40F9Qo6B20lvlsALamZmdIEbb+q2p0v77BahpaeRG9jzHmvY/H4o82YUqhBtor4skhptppqQCTr6FFVMIWta4kEPBIg3EGDO421j5LHGzDZIMEhschqdNCTPminFpVZid9AzQo4wugLhXukaC4jVDEI2tRMZuaDcuH5NubbCiyB6iqVFPYdD5j1eVgCrgrtqZA0aZlenWuspVJXnMzjY/wuKTKjWleVo1CnWCqKiE7IM+FLY9pLdoQ2GcjWBMOZLTOtat6bVVrVwVYWC+zx/t3Qy1GPAHiEOPVsxHkV5pi9f7btL6bSNGul3mIXlKBAInmJHMTdYtyPpPAleBfY2dQaWF7XAZcoLSfE5xmS0clbLAAve4OluymPNPOTTBDTpJnvC47EFwEx4RAtty67+qdqymN9huAJaQ6AYuA67SRbSb6rVz2hpmQ60BsZS3VwN7XiNVSlUa67Zbka1pBcDmJJkt/N1V7ZBMRlEuPnA0FpkBNj0e/uDYb/wCw1JIhpb3lDGmMwc4xBzTIBkXHzRuGaXZoEhvicRGmgt3J9eiE4jRFxM9oIJ6eUfNJnFcW0gl2dqcZcHuewh9RxJe9zG3LvitEHVDYjiTnuHv6j6nQEQOw+EeQQtXCkIc04K5GXLmWmv8AwZGMfQ1/+cawRRohp0zvPvHdwIgH1QFPFvD/AHge4PNy4OId66rDIo5inllyydt9fsFSGNfiuJqfFXqH/kWjzhVp0/dlubUmTPUWPUIKm10pjWwxgFxBdIDYuIiZlV+Pc7nTte2C9aB6r75R5lNMLRFpBDSQCYJj+4ul1PDGfy6Z4ao6GsLvDMtBgDMYEk/dX+MpW3JAS+xo/Dt0zifHIcC0eEeG+5N7LENMTFp1i3OPoj64aco8Lc1y58fEJBAI0bolrqlumsdeyqlSBReIE6TbnJF/LUIWs6StK9S97mBBGmgj5IV7lNlmgkWLlwXMKgk6LuWFLkzUa3QzqUrc0oxmHi4TDC4mbFdxTQk5IRyxsng5QlTESiIqUbqLmvHIs5IYAqwKzULl0eRJRvKgas2FbhHHYD0daEfhqsIFq1Dk2LoTNWekweIlOMIZXjcBiIdC9XgaohURlaOX5OLixk+wSvGV4RlauIXmuL4i8ArXpCsGPlKivEMWXNLeYIXmG2KbkoLEsBMjzQKe9na8WofKcZhs2hB8JdrEdENTMkA2ki/Ja4xmVxy6WiLoSqYTMk0v2LI7HOCoSKpY4HLBi1xPOeQNxOizrvGU6m21gDI9f7SejXgiTAm6YMrai92xbc639PkixZ4zWjXFo34W9uYhxdEQcoDjBzQI3vCpXdOtyTBOpgfnyWWBdBcBcmALTry5FEClmLogQC65DTDfO7uiZB3Ex6ZSgPe5g4y5v+R1btc77QhMThptpyVqzixzXtmR6EciihjKT7yGndrjoe51SnwlcJ/7QW+0KCADDrHnsVZ1Ec+yYvwBrGWw7nlIt9lSlh2hvw6HUa9o/lJ/DO2qVeme5mdGkTFpdMRBm15RD25j0GgXaJLSHMdBgkkEgtmQWyd4+q2wzd1XCHoEGe2COi0w5AJzNzCHACctyPCfI3VX+J8AjlcwPMoghhLcgcPCM0kOl98xEbLfZ5mWOc4NbABiRoATmEyefRLXMedk04iMoa0ggnxXEW/aRzm/ogqtX6X7pOZJvbYSBwwjVdoYYuMmwW+FMuk6D59Cis4lRTp9MXPI1pFWUABAWNWmi5WdQIJRVCFJ2LXiFo2vNipXCDcVLKTg9FKXJBJaurFldREpwfs2mGEKsLchVypjQlM40LUFUhQIk6BezVpUL1mSqyicjKNhUTfh3EjokjWyV6XhGBZE7p2Dk2T+TwUdhzXucEi4o0hy9SylCXcT4dnVU42tEGHKoz30eaqkwshh3HReip8LGiY0eHtA0U7wNvbKn5cY9HmsLwuo4RtyKZ0fZtp+IJ3TytQeO40GaGU5RjFbE/ics3UTtL2XoAeJgK85xjAjD1HXIaSDSy3kH4hP7SEbU9qXGwCqzD1MSCcxBiW9FkZxb+UqxSyQd5Ho83SqFrpGhsexTZ8EABrWlrYcQT4iJvffsk2Nw7qbi14gj59ua34bxBvhY4RFmuGpvMO+gWYs8Yz4S9nSatWgt1IkOMaAT02CW1sJunIotIkuvsN//wBco1CNZg3M8NQOptcPEcs5m9BvCpyYY5FTMUqFWG4VV917wN8IF7xqToN1ak61wDY6315dUx/RuaHMZWe1juYIkayW9UufiK+VzSacBoaJaM0TPg5FeS4KkjLbOYqoDAgAgBtgB4W843mbqPr5G9do5/wsKVMgFzpNrnrbX1WbjmP8rHN19wkktEoWLS4SJkjmdYPdG06wa4uIgAuMAkRrYHVDteQA2bTmubDa4Up4N1VpANpv5f7QuXCOuwZSS3IAxGOc92ZxJsA2TMNGgHRdw9N1Q9N+ydYT2faLvKvxENa3KwQoOGRq5sU/Ki3xgLa9QCwEALNlRYlSVO57DUVQYKi46ohBUXDUXviGcC1dyCeVq9yxIUuWVsfBUUUXYXEgaeidTVMi3WLiuxJI5ibK5FUhbBVehaCTMlxRxWZJS2w0jRok2XouH5mtEJRw7DEm69PQo5QOSr8eL7IvLyL9Ibh3khX92UC3HtaYlFPxrSNVbRzJQkvRZtJSrVDRqhamLAFiEix9eo462S5zURmPC5vYbjsdMwV5zFOLitnPcTCPwnDyVFJvI6OjBRwqwXA8PnVMsRjTQbAR9LDimJK83x/Gh5geac0sWO/YMJPNPfQsx+LNZxLvJAFkIjKumlK5juTuXZ1YNRVIo3GVAIDvoVvS43iAQTUc8CwDyX23AJuEPUYBzlYkI5TywaqT/kZSZ6E+1Gac1IjYQ8vgDYZlKXF6BJztqRlMZQJz7b6LzsLoamx87OtPZnCJviMQ52p8hoE0wvGvd4c0DTDpuHcvuUvwuHnlpN10tE9EcXkheRvbBlxfZocSXxDYjU/ZOMBXgQk7SNrIijUhejmk5XJk+Zc1Q+dXsl2KuuNrKr3SnSnaJIQ4sBqU0NUKOqoKqFDkRbBmOZVLlxyqpHJlCQVg6GcgL1+F9mm5ZjVeQ4XiMlQHZfUOG1w9gI5LqeDGEoXWzm+dknBquhAPZlnJdXoXuuorfhx+hD+IyfU8E51liXKKLnzZ0YonvFV1RRRLcmMpGbTJher4Xw2mW3EqKKzxEmm2SedJxiqAOKPLKjWsH+k7pVpYAVFE7H+pkuVflxYrxWDkzKwc12xUURyQUJugd+YbqtNrnGAVFFLJboov5bGeGwAFymdBvJRRPhFIgnJy7F3tFjYbAXkHOUUUflSfOjreJFLHo5Ks0qKKayphDYOoVH4Zh2jsoomdoVbT0Z/pG8yuvDYiFFEt66Dtvsyc5Z51FEiUmMSLsctmOUUTIMGSCGvV866oqUxDRm9yGqriiCQcAV4WZUUUMlspRVew9lOLH4CooqvBm45aXsR5cFLG7PUPkqKKLtnBP//Z' - -class InitialFileFromDataUrl extends React.Component { - constructor() { - super() - this.state = { file: undefined } - this.handleClick = this.handleClick.bind(this) - this.getUploadParams = this.getUploadParams.bind(this) - this.handleSubmit = this.handleSubmit.bind(this) - } - - handleClick() { - fetch(imageDataUrl).then(res => { - res.arrayBuffer().then(buf => { - const file = new File([buf], 'image_data_url.jpg', { type: 'image/jpeg' }) - this.setState({ file }) - }) - }) - } - - getUploadParams() { - return { url: 'https://httpbin.org/post' } - } - - handleSubmit(files, allFiles) { - console.log(files.map(f => f.meta)) - allFiles.forEach(f => f.remove()) - this.setState({ file: undefined }) - } - - render() { - const { file } = this.state - if (!file) - return ( -
- Click me to upload a file from a data URL. -
- ) - - return ( - - ) - } -} - - -~~~ +User can choose files with input, but can't drop them. + +~~~js +const imageDataUrl = 'data:image/jpeg;base64,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' + +class InitialFileFromDataUrl extends React.Component { + constructor() { + super() + this.state = { file: undefined } + this.handleClick = this.handleClick.bind(this) + this.getUploadParams = this.getUploadParams.bind(this) + this.handleSubmit = this.handleSubmit.bind(this) + } + + handleClick() { + fetch(imageDataUrl).then(res => { + res.arrayBuffer().then(buf => { + const file = new File([buf], 'image_data_url.jpg', { type: 'image/jpeg' }) + this.setState({ file }) + }) + }) + } + + getUploadParams() { + return { url: 'https://httpbin.org/post' } + } + + handleSubmit(files, allFiles) { + console.log(files.map(f => f.meta)) + allFiles.forEach(f => f.remove()) + this.setState({ file: undefined }) + } + + render() { + const { file } = this.state + if (!file) + return ( +
+ Click me to upload a file from a data URL. +
+ ) + + return ( + + ) + } +} + + +~~~ diff --git a/examples/NoDropzone.md b/examples/NoDropzone.md index e27ceb4..aecee50 100644 --- a/examples/NoDropzone.md +++ b/examples/NoDropzone.md @@ -1,36 +1,36 @@ -User can choose files with input, but can't drop them. - -~~~js -const NoDropzoneLayout = ({ previews, submitButton, input, files, dropzoneProps }) => { - const { ref, className, style } = dropzoneProps - return ( -
- {previews} - - {input} - - {files.length > 0 && submitButton} -
- ) -} - -const InputNoDropzone = () => { - const getUploadParams = () => ({ url: 'https://httpbin.org/post' }) - - const handleSubmit = (files, allFiles) => { - console.log(files.map(f => f.meta)) - allFiles.forEach(f => f.remove()) - } - - return ( - - ) -} - - -~~~ +User can choose files with input, but can't drop them. + +~~~js +const NoDropzoneLayout = ({ previews, submitButton, input, files, dropzoneProps }) => { + const { ref, className, style } = dropzoneProps + return ( +
+ {previews} + + {input} + + {files.length > 0 && submitButton} +
+ ) +} + +const InputNoDropzone = () => { + const getUploadParams = () => ({ url: 'https://httpbin.org/post' }) + + const handleSubmit = (files, allFiles) => { + console.log(files.map(f => f.meta)) + allFiles.forEach(f => f.remove()) + } + + return ( + + ) +} + + +~~~ diff --git a/examples/NoInput.md b/examples/NoInput.md index 5736696..eb564fc 100644 --- a/examples/NoInput.md +++ b/examples/NoInput.md @@ -1,40 +1,40 @@ -If for some reason you want to do this... - -~~~js -const { defaultClassNames } = require('../src/Dropzone') - -const NoInputLayout = ({ previews, submitButton, dropzoneProps, files }) => { - return ( -
- {files.length === 0 && - - Only Drop Files (No Input) - - } - - {previews} - - {files.length > 0 && submitButton} -
- ) -} - -const DropzoneNoInput = () => { - const getUploadParams = () => ({ url: 'https://httpbin.org/post' }) - - const handleSubmit = (files, allFiles) => { - console.log(files.map(f => f.meta)) - allFiles.forEach(f => f.remove()) - } - - return ( - - ) -} - - -~~~ +If for some reason you want to do this... + +~~~js +const { defaultClassNames } = require('../src/Dropzone') + +const NoInputLayout = ({ previews, submitButton, dropzoneProps, files }) => { + return ( +
+ {files.length === 0 && + + Only Drop Files (No Input) + + } + + {previews} + + {files.length > 0 && submitButton} +
+ ) +} + +const DropzoneNoInput = () => { + const getUploadParams = () => ({ url: 'https://httpbin.org/post' }) + + const handleSubmit = (files, allFiles) => { + console.log(files.map(f => f.meta)) + allFiles.forEach(f => f.remove()) + } + + return ( + + ) +} + + +~~~ diff --git a/examples/NoUpload.md b/examples/NoUpload.md index 23ecedc..bcdc011 100644 --- a/examples/NoUpload.md +++ b/examples/NoUpload.md @@ -1,27 +1,27 @@ -Doesn't upload files. Disables submit button until 3 files have been dropped, dynamically updates number of remaining files. - -~~~js -const NoUpload = () => { - const handleChangeStatus = ({ meta }, status) => { - console.log(status, meta) - } - - const handleSubmit = (files, allFiles) => { - console.log(files.map(f => f.meta)) - allFiles.forEach(f => f.remove()) - } - - return ( - `${3 - files.length} more`} - submitButtonDisabled={files => files.length < 3} - /> - ) -} - - -~~~ +Doesn't upload files. Disables submit button until 3 files have been dropped, dynamically updates number of remaining files. + +~~~js +const NoUpload = () => { + const handleChangeStatus = ({ meta }, status) => { + console.log(status, meta) + } + + const handleSubmit = (files, allFiles) => { + console.log(files.map(f => f.meta)) + allFiles.forEach(f => f.remove()) + } + + return ( + `${3 - files.length} more`} + submitButtonDisabled={files => files.length < 3} + /> + ) +} + + +~~~ diff --git a/examples/Quickstart.md b/examples/Quickstart.md index a5c9930..bc03705 100644 --- a/examples/Quickstart.md +++ b/examples/Quickstart.md @@ -1,31 +1,31 @@ -~~~js -/** -import 'react-dropzone-uploader/dist/styles.css' -import Dropzone from 'react-dropzone-uploader' -**/ - -const MyUploader = () => { - // specify upload params and url for your files - const getUploadParams = ({ meta }) => { return { url: 'https://httpbin.org/post' } } - - // called every time a file's `status` changes - const handleChangeStatus = ({ meta, file }, status) => { console.log(status, meta, file) } - - // receives array of files that are done uploading when submit button is clicked - const handleSubmit = (files, allFiles) => { - console.log(files.map(f => f.meta)) - allFiles.forEach(f => f.remove()) - } - - return ( - - ) -} - - -~~~ +~~~js +/** +import 'react-dropzone-uploader/dist/styles.css' +import Dropzone from 'react-dropzone-uploader' +**/ + +const MyUploader = () => { + // specify upload params and url for your files + const getUploadParams = ({ meta }) => { return { url: 'https://httpbin.org/post' } } + + // called every time a file's `status` changes + const handleChangeStatus = ({ meta, file }, status) => { console.log(status, meta, file) } + + // receives array of files that are done uploading when submit button is clicked + const handleSubmit = (files, allFiles) => { + console.log(files.map(f => f.meta)) + allFiles.forEach(f => f.remove()) + } + + return ( + + ) +} + + +~~~ diff --git a/examples/SingleFile.md b/examples/SingleFile.md index 7a1528e..826b4b9 100644 --- a/examples/SingleFile.md +++ b/examples/SingleFile.md @@ -1,47 +1,47 @@ -Automatically removes file from dropzone when it finishes uploading. Limits dropzone to 1 file using `maxFiles` prop. Doesn't include submit button. - -Changes border color for "active" dropzone using `styles` prop. - -~~~js -const SingleFileAutoSubmit = () => { - const toast = (innerHTML) => { - const el = document.getElementById('toast') - el.innerHTML = innerHTML - el.className = 'show' - setTimeout(() => { el.className = el.className.replace('show', '') }, 3000) - } - - const getUploadParams = () => { - return { url: 'https://httpbin.org/post' } - } - - const handleChangeStatus = ({ meta, remove }, status) => { - if (status === 'headers_received') { - toast(`${meta.name} uploaded!`) - remove() - } else if (status === 'aborted') { - toast(`${meta.name}, upload failed...`) - } - } - - return ( - -
Upload
- -
- ) -} - - -~~~ +Automatically removes file from dropzone when it finishes uploading. Limits dropzone to 1 file using `maxFiles` prop. Doesn't include submit button. + +Changes border color for "active" dropzone using `styles` prop. + +~~~js +const SingleFileAutoSubmit = () => { + const toast = (innerHTML) => { + const el = document.getElementById('toast') + el.innerHTML = innerHTML + el.className = 'show' + setTimeout(() => { el.className = el.className.replace('show', '') }, 3000) + } + + const getUploadParams = () => { + return { url: 'https://httpbin.org/post' } + } + + const handleChangeStatus = ({ meta, remove }, status) => { + if (status === 'headers_received') { + toast(`${meta.name} uploaded!`) + remove() + } else if (status === 'aborted') { + toast(`${meta.name}, upload failed...`) + } + } + + return ( + +
Upload
+ +
+ ) +} + + +~~~ diff --git a/examples/Standard.md b/examples/Standard.md index 7985b76..101cd10 100644 --- a/examples/Standard.md +++ b/examples/Standard.md @@ -1,31 +1,31 @@ -Uploads files to . Logs file metadata to console on submit, and removes files from dropzone using `fileWithMeta.remove`. - -Limits dropzone height with `styles` prop. - -~~~js -const Standard = () => { - const getUploadParams = () => { - return { url: 'https://httpbin.org/post' } - } - - const handleChangeStatus = ({ meta }, status) => { - console.log(status, meta) - } - - const handleSubmit = (files, allFiles) => { - console.log(files.map(f => f.meta)) - allFiles.forEach(f => f.remove()) - } - - return ( - - ) -} - - -~~~ +Uploads files to . Logs file metadata to console on submit, and removes files from dropzone using `fileWithMeta.remove`. + +Limits dropzone height with `styles` prop. + +~~~js +const Standard = () => { + const getUploadParams = () => { + return { url: 'https://httpbin.org/post' } + } + + const handleChangeStatus = ({ meta }, status) => { + console.log(status, meta) + } + + const handleSubmit = (files, allFiles) => { + console.log(files.map(f => f.meta)) + allFiles.forEach(f => f.remove()) + } + + return ( + + ) +} + + +~~~ diff --git a/examples/dist/index.html b/examples/dist/index.html index 4876981..15b7fab 100644 --- a/examples/dist/index.html +++ b/examples/dist/index.html @@ -1,28 +1,28 @@ - - - - - React Dropzone Uploader - - - - - -
-
-
-
-
-
-
-
-
-
-
-
-
- - - - - + + + + + React Dropzone Uploader + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + diff --git a/examples/src/data.ts b/examples/src/data.ts index 26db580..21f8736 100644 --- a/examples/src/data.ts +++ b/examples/src/data.ts @@ -1,3 +1,3 @@ -const imageDataUrl = 'data:image/jpeg;base64,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' - -export { imageDataUrl } +const imageDataUrl = 'data:image/jpeg;base64,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' + +export { imageDataUrl } diff --git a/examples/src/index.tsx b/examples/src/index.tsx index 9007d8f..23bc09d 100644 --- a/examples/src/index.tsx +++ b/examples/src/index.tsx @@ -1,380 +1,380 @@ -//@ts-ignore -export = null - -import 'babel-polyfill' - -import React from 'react' -import ReactDOM from 'react-dom' -//@ts-ignore -import { getDroppedOrSelectedFiles } from 'html5-file-selector' -//@ts-ignore -import { ToastContainer, toast } from 'react-toastify' -import 'react-toastify/dist/ReactToastify.css' - -import '../../src/styles.css' -import Dropzone, { - defaultClassNames, - IDropzoneProps, - ILayoutProps, - IPreviewProps, - IInputProps, -} from '../../src/Dropzone' -import { imageDataUrl } from './data' - -const Standard = () => { - const getUploadParams: IDropzoneProps['getUploadParams'] = () => { - return { url: 'https://httpbin.org/post' } - } - - const handleChangeStatus: IDropzoneProps['onChangeStatus'] = ({ meta }, status) => { - console.log(status, meta) - } - - const handleSubmit: IDropzoneProps['onSubmit'] = (files, allFiles) => { - console.log(files.map(f => f.meta)) - allFiles.forEach(f => f.remove()) - } - - return ( - - ) -} - -const ImageAudioVideo = () => { - const getUploadParams = ({ meta }) => { - const url = 'https://httpbin.org/post' - return { url, meta: { fileUrl: `${url}/${encodeURIComponent(meta.name)}` } } - } - - const handleChangeStatus = ({ meta }, status) => { - console.log(status, meta) - } - - const handleSubmit = (files, allFiles) => { - console.log(files.map(f => f.meta)) - allFiles.forEach(f => f.remove()) - } - - return ( - (reject ? 'Image, audio and video files only' : 'Drag Files')} - styles={{ - dropzoneReject: { borderColor: 'red', backgroundColor: '#DAA' }, - inputLabel: (files, { reject }) => (reject ? { color: 'red' } : {}), - }} - /> - ) -} - -const NoUpload = () => { - const handleChangeStatus = ({ meta }, status) => { - console.log(status, meta) - } - - const handleSubmit = (files, allFiles) => { - console.log(files.map(f => f.meta)) - allFiles.forEach(f => f.remove()) - } - - return ( - `${3 - files.length} more`} - submitButtonDisabled={files => files.length < 3} - /> - ) -} - -const SingleFileAutoSubmit = () => { - const getUploadParams = () => { - return { url: 'https://httpbin.org/post' } - } - - const handleChangeStatus = ({ meta, remove }, status) => { - if (status === 'headers_received') { - toast.success(`${meta.name} uploaded!`) - remove() - } else if (status === 'aborted') { - toast.error(`${meta.name}, upload failed...`) - } - } - - return ( - - - - - ) -} - -const Preview = ({ meta }: IPreviewProps) => { - const { name, percent, status } = meta - return ( - - {name}, {Math.round(percent)}%, {status} - - ) -} - -const CustomPreview = () => { - const getUploadParams = () => ({ url: 'https://httpbin.org/post' }) - - const handleSubmit = (files, allFiles) => { - console.log(files.map(f => f.meta)) - allFiles.forEach(f => f.remove()) - } - - return ( - files.some(f => ['preparing', 'getting_upload_params', 'uploading'].includes(f.meta.status))} - /> - ) -} - -const Layout = ({ input, previews, submitButton, dropzoneProps, files, extra: { maxFiles } }: ILayoutProps) => { - return ( -
- {previews} - -
{files.length < maxFiles && input}
- - {files.length > 0 && submitButton} -
- ) -} - -const CustomLayout = () => { - const getUploadParams = () => ({ url: 'https://httpbin.org/post' }) - - const handleSubmit = (files, allFiles) => { - console.log(files.map(f => f.meta)) - allFiles.forEach(f => f.remove()) - } - - return ( - - ) -} - -const Input = ({ accept, onFiles, files, getFilesFromEvent }: IInputProps) => { - const text = files.length > 0 ? 'Add more files' : 'Choose files' - - return ( - - ) -} - -const CustomInput = () => { - const handleSubmit = (files, allFiles) => { - console.log(files.map(f => f.meta)) - allFiles.forEach(f => f.remove()) - } - - const getFilesFromEvent = async e => { - const chosenFiles = await getDroppedOrSelectedFiles(e) - return chosenFiles.map(f => f.fileObject) - } - - return ( - ({ url: 'https://httpbin.org/post' })} - onSubmit={handleSubmit} - InputComponent={Input} - getFilesFromEvent={getFilesFromEvent} - /> - ) -} - -const NoInputLayout = ({ previews, submitButton, dropzoneProps, files }: ILayoutProps) => { - return ( -
- {files.length === 0 && ( - - Only Drop Files (No Input) - - )} - - {previews} - - {files.length > 0 && submitButton} -
- ) -} - -const DropzoneNoInput = () => { - const getUploadParams = () => ({ url: 'https://httpbin.org/post' }) - - const handleSubmit = (files, allFiles) => { - console.log(files.map(f => f.meta)) - allFiles.forEach(f => f.remove()) - } - - return -} - -const NoDropzoneLayout = ({ previews, submitButton, input, files, dropzoneProps }: ILayoutProps) => { - const { ref, className, style } = dropzoneProps - - return ( -

- {previews} - - {input} - - {files.length > 0 && submitButton} -

- ) -} - -const InputNoDropzone = () => { - const getUploadParams = () => ({ url: 'https://httpbin.org/post' }) - - const handleSubmit = (files, allFiles) => { - console.log(files.map(f => f.meta)) - allFiles.forEach(f => f.remove()) - } - - return ( - - ) -} - -class InitialFileFromDataUrl extends React.Component { - state = { file: undefined } - - handleClick = async () => { - const res = await fetch(imageDataUrl) - const buf = await res.arrayBuffer() - const file = new File([buf], 'image_data_url.jpg', { type: 'image/jpeg' }) - this.setState({ file }) - } - - handleSubmit = (files, allFiles) => { - console.log(files.map(f => f.meta)) - allFiles.forEach(f => f.remove()) - this.setState({ file: undefined }) - } - - render() { - const { file } = this.state - if (!file) - return ( -
- Click me to upload a file from a data URL. -
- ) - - return ( - ({ url: 'https://httpbin.org/post' })} - InputComponent={null} - onSubmit={this.handleSubmit} - initialFiles={[file]} - canCancel={false} - canRemove={false} - canRestart={false} - /> - ) - } -} - -const OnFilesLayout = ({ previews, submitButton, dropzoneProps, files, extra }: ILayoutProps) => { - const handleClick = async () => { - const res = await fetch(imageDataUrl) - const buf = await res.arrayBuffer() - const file = new File([buf], 'image_data_url.jpg', { type: 'image/jpeg' }) - extra.onFiles([file]) - } - - return ( -
- - {previews} - - {files.length > 0 && submitButton} -
- ) -} - -const CustomOnFiles = () => { - return ( - ({ url: 'https://httpbin.org/post' })} - LayoutComponent={OnFilesLayout} - onSubmit={(files, allFiles) => { - console.log(files.map(f => f.meta)) - allFiles.forEach(f => f.remove()) - }} - canCancel={false} - canRemove={false} - canRestart={false} - /> - ) -} - -ReactDOM.render(, document.getElementById('example-1')) -ReactDOM.render(, document.getElementById('example-2')) -ReactDOM.render(, document.getElementById('example-3')) -ReactDOM.render(, document.getElementById('example-4')) -ReactDOM.render(, document.getElementById('example-5')) -ReactDOM.render(, document.getElementById('example-6')) -ReactDOM.render(, document.getElementById('example-7')) -ReactDOM.render(, document.getElementById('example-8')) -ReactDOM.render(, document.getElementById('example-9')) -ReactDOM.render(, document.getElementById('example-10')) -ReactDOM.render(, document.getElementById('example-11')) +//@ts-ignore +export = null + +import 'babel-polyfill' + +import React from 'react' +import ReactDOM from 'react-dom' +//@ts-ignore +import { getDroppedOrSelectedFiles } from 'html5-file-selector' +//@ts-ignore +import { ToastContainer, toast } from 'react-toastify' +import 'react-toastify/dist/ReactToastify.css' + +import '../../src/styles.css' +import Dropzone, { + defaultClassNames, + IDropzoneProps, + ILayoutProps, + IPreviewProps, + IInputProps, +} from '../../src/Dropzone' +import { imageDataUrl } from './data' + +const Standard = () => { + const getUploadParams: IDropzoneProps['getUploadParams'] = () => { + return { url: 'https://httpbin.org/post' } + } + + const handleChangeStatus: IDropzoneProps['onChangeStatus'] = ({ meta }, status) => { + console.log(status, meta) + } + + const handleSubmit: IDropzoneProps['onSubmit'] = (files, allFiles) => { + console.log(files.map(f => f.meta)) + allFiles.forEach(f => f.remove()) + } + + return ( + + ) +} + +const ImageAudioVideo = () => { + const getUploadParams = ({ meta }) => { + const url = 'https://httpbin.org/post' + return { url, meta: { fileUrl: `${url}/${encodeURIComponent(meta.name)}` } } + } + + const handleChangeStatus = ({ meta }, status) => { + console.log(status, meta) + } + + const handleSubmit = (files, allFiles) => { + console.log(files.map(f => f.meta)) + allFiles.forEach(f => f.remove()) + } + + return ( + (reject ? 'Image, audio and video files only' : 'Drag Files')} + styles={{ + dropzoneReject: { borderColor: 'red', backgroundColor: '#DAA' }, + inputLabel: (files, { reject }) => (reject ? { color: 'red' } : {}), + }} + /> + ) +} + +const NoUpload = () => { + const handleChangeStatus = ({ meta }, status) => { + console.log(status, meta) + } + + const handleSubmit = (files, allFiles) => { + console.log(files.map(f => f.meta)) + allFiles.forEach(f => f.remove()) + } + + return ( + `${3 - files.length} more`} + submitButtonDisabled={files => files.length < 3} + /> + ) +} + +const SingleFileAutoSubmit = () => { + const getUploadParams = () => { + return { url: 'https://httpbin.org/post' } + } + + const handleChangeStatus = ({ meta, remove }, status) => { + if (status === 'headers_received') { + toast.success(`${meta.name} uploaded!`) + remove() + } else if (status === 'aborted') { + toast.error(`${meta.name}, upload failed...`) + } + } + + return ( + + + + + ) +} + +const Preview = ({ meta }: IPreviewProps) => { + const { name, percent, status } = meta + return ( + + {name}, {Math.round(percent)}%, {status} + + ) +} + +const CustomPreview = () => { + const getUploadParams = () => ({ url: 'https://httpbin.org/post' }) + + const handleSubmit = (files, allFiles) => { + console.log(files.map(f => f.meta)) + allFiles.forEach(f => f.remove()) + } + + return ( + files.some(f => ['preparing', 'getting_upload_params', 'uploading'].includes(f.meta.status))} + /> + ) +} + +const Layout = ({ input, previews, submitButton, dropzoneProps, files, extra: { maxFiles } }: ILayoutProps) => { + return ( +
+ {previews} + +
{files.length < maxFiles && input}
+ + {files.length > 0 && submitButton} +
+ ) +} + +const CustomLayout = () => { + const getUploadParams = () => ({ url: 'https://httpbin.org/post' }) + + const handleSubmit = (files, allFiles) => { + console.log(files.map(f => f.meta)) + allFiles.forEach(f => f.remove()) + } + + return ( + + ) +} + +const Input = ({ accept, onFiles, files, getFilesFromEvent }: IInputProps) => { + const text = files.length > 0 ? 'Add more files' : 'Choose files' + + return ( + + ) +} + +const CustomInput = () => { + const handleSubmit = (files, allFiles) => { + console.log(files.map(f => f.meta)) + allFiles.forEach(f => f.remove()) + } + + const getFilesFromEvent = async e => { + const chosenFiles = await getDroppedOrSelectedFiles(e) + return chosenFiles.map(f => f.fileObject) + } + + return ( + ({ url: 'https://httpbin.org/post' })} + onSubmit={handleSubmit} + InputComponent={Input} + getFilesFromEvent={getFilesFromEvent} + /> + ) +} + +const NoInputLayout = ({ previews, submitButton, dropzoneProps, files }: ILayoutProps) => { + return ( +
+ {files.length === 0 && ( + + Only Drop Files (No Input) + + )} + + {previews} + + {files.length > 0 && submitButton} +
+ ) +} + +const DropzoneNoInput = () => { + const getUploadParams = () => ({ url: 'https://httpbin.org/post' }) + + const handleSubmit = (files, allFiles) => { + console.log(files.map(f => f.meta)) + allFiles.forEach(f => f.remove()) + } + + return +} + +const NoDropzoneLayout = ({ previews, submitButton, input, files, dropzoneProps }: ILayoutProps) => { + const { ref, className, style } = dropzoneProps + + return ( +

+ {previews} + + {input} + + {files.length > 0 && submitButton} +

+ ) +} + +const InputNoDropzone = () => { + const getUploadParams = () => ({ url: 'https://httpbin.org/post' }) + + const handleSubmit = (files, allFiles) => { + console.log(files.map(f => f.meta)) + allFiles.forEach(f => f.remove()) + } + + return ( + + ) +} + +class InitialFileFromDataUrl extends React.Component { + state = { file: undefined } + + handleClick = async () => { + const res = await fetch(imageDataUrl) + const buf = await res.arrayBuffer() + const file = new File([buf], 'image_data_url.jpg', { type: 'image/jpeg' }) + this.setState({ file }) + } + + handleSubmit = (files, allFiles) => { + console.log(files.map(f => f.meta)) + allFiles.forEach(f => f.remove()) + this.setState({ file: undefined }) + } + + render() { + const { file } = this.state + if (!file) + return ( +
+ Click me to upload a file from a data URL. +
+ ) + + return ( + ({ url: 'https://httpbin.org/post' })} + InputComponent={null} + onSubmit={this.handleSubmit} + initialFiles={[file]} + canCancel={false} + canRemove={false} + canRestart={false} + /> + ) + } +} + +const OnFilesLayout = ({ previews, submitButton, dropzoneProps, files, extra }: ILayoutProps) => { + const handleClick = async () => { + const res = await fetch(imageDataUrl) + const buf = await res.arrayBuffer() + const file = new File([buf], 'image_data_url.jpg', { type: 'image/jpeg' }) + extra.onFiles([file]) + } + + return ( +
+ + {previews} + + {files.length > 0 && submitButton} +
+ ) +} + +const CustomOnFiles = () => { + return ( + ({ url: 'https://httpbin.org/post' })} + LayoutComponent={OnFilesLayout} + onSubmit={(files, allFiles) => { + console.log(files.map(f => f.meta)) + allFiles.forEach(f => f.remove()) + }} + canCancel={false} + canRemove={false} + canRestart={false} + /> + ) +} + +ReactDOM.render(, document.getElementById('example-1')) +ReactDOM.render(, document.getElementById('example-2')) +ReactDOM.render(, document.getElementById('example-3')) +ReactDOM.render(, document.getElementById('example-4')) +ReactDOM.render(, document.getElementById('example-5')) +ReactDOM.render(, document.getElementById('example-6')) +ReactDOM.render(, document.getElementById('example-7')) +ReactDOM.render(, document.getElementById('example-8')) +ReactDOM.render(, document.getElementById('example-9')) +ReactDOM.render(, document.getElementById('example-10')) +ReactDOM.render(, document.getElementById('example-11')) diff --git a/examples/styles.css b/examples/styles.css index 88dc422..a41dccb 100644 --- a/examples/styles.css +++ b/examples/styles.css @@ -1,42 +1,42 @@ -#toast { - visibility: hidden; - min-width: 250px; - margin-left: -125px; - background-color: #333; - color: #fff; - text-align: center; - border-radius: 2px; - padding: 16px; - position: fixed; - z-index: 1; - right: 10%; - bottom: 30px; - font-size: 17px; - font-family: 'Helvetica', sans-serif; -} - -#toast.show { - visibility: visible; - -webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s; - animation: fadein 0.5s, fadeout 0.5s 2.5s; -} - -@-webkit-keyframes fadein { - from {opacity: 0;} - to {opacity: 1;} -} - -@keyframes fadein { - from {opacity: 0;} - to {opacity: 1;} -} - -@-webkit-keyframes fadeout { - from {opacity: 1;} - to {opacity: 0;} -} - -@keyframes fadeout { - from {opacity: 1;} - to {opacity: 0;} -} +#toast { + visibility: hidden; + min-width: 250px; + margin-left: -125px; + background-color: #333; + color: #fff; + text-align: center; + border-radius: 2px; + padding: 16px; + position: fixed; + z-index: 1; + right: 10%; + bottom: 30px; + font-size: 17px; + font-family: 'Helvetica', sans-serif; +} + +#toast.show { + visibility: visible; + -webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s; + animation: fadein 0.5s, fadeout 0.5s 2.5s; +} + +@-webkit-keyframes fadein { + from {opacity: 0;} + to {opacity: 1;} +} + +@keyframes fadein { + from {opacity: 0;} + to {opacity: 1;} +} + +@-webkit-keyframes fadeout { + from {opacity: 1;} + to {opacity: 0;} +} + +@keyframes fadeout { + from {opacity: 1;} + to {opacity: 0;} +} diff --git a/package.json b/package.json index c11b47b..2d0bf1b 100644 --- a/package.json +++ b/package.json @@ -1,90 +1,90 @@ -{ - "name": "react-dropzone-uploader", - "version": "2.11.0", - "author": "Kyle Bebak ", - "description": "React file dropzone and uploader: fully customizable, progress indicators, upload cancellation and restart, zero deps and excellent TypeScript support", - "main": "./dist/react-dropzone-uploader.js", - "types": "./dist/Dropzone.tsx", - "keywords": [ - "react", - "react-component", - "file", - "HTML5", - "input", - "dropzone", - "uploader", - "progress", - "typescript", - "s3" - ], - "repository": { - "type": "git", - "url": "git://github.com/fortana-co/react-dropzone-uploader.git" - }, - "license": "MIT", - "peerDependencies": { - "react": ">=16.2.0", - "react-dom": ">=16.2.0", - "prop-types": ">=15.5.10" - }, - "devDependencies": { - "@babel/core": "^7.1.2", - "@babel/plugin-proposal-class-properties": "^7.1.0", - "@babel/plugin-proposal-function-bind": "^7.0.0", - "@babel/plugin-proposal-object-rest-spread": "^7.0.0", - "@babel/plugin-transform-runtime": "^7.1.0", - "@babel/preset-env": "^7.1.0", - "@babel/preset-react": "^7.0.0", - "@types/react": "^16.8.10", - "@types/react-dom": "^16.8.3", - "babel-core": "^7.0.0-bridge.0", - "babel-jest": "^23.6.0", - "babel-loader": "^8.0.4", - "babel-polyfill": "^6.26.0", - "css-loader": "^1.0.0", - "docusaurus-init": "^1.0.2", - "enzyme": "^3.7.0", - "enzyme-adapter-react-16": "^1.7.0", - "html5-file-selector": "^2.1.0", - "husky": "^1.3.1", - "jest": "^23.6.0", - "prettier": "1.16.4", - "react": ">=16.2.0", - "react-dom": ">=16.2.0", - "react-styleguidist": "^8.0.6", - "react-test-renderer": "^16.6.3", - "react-toastify": "^4.5.2", - "style-loader": "^0.23.1", - "typescript": "^3.4.1", - "uglifyjs-webpack-plugin": "^2.0.1", - "url-loader": "^1.1.2", - "webpack": "^4.21.0", - "webpack-cli": "^3.1.2", - "webpack-dev-server": "^3.1.14" - }, - "jest": { - "moduleNameMapper": { - "\\.(png|jpg|jpeg|gif|svg|woff|woff2)$": "/tests/fileMock.js" - } - }, - "scripts": { - "build": "rm dist/* && tsc && NODE_ENV=production webpack --config webpack.build.config.js && cp src/styles.css dist/styles.css && cp src/*.ts* dist", - "dev": "tsc -w -p tsconfig.dev.json & webpack-dev-server --config webpack.config.js --mode development --open", - "styleguide": "tsc -w & styleguidist server", - "build-styleguide": "tsc && styleguidist build", - "styleguide-quickstart": "tsc -w && styleguidist server --config styleguide-quickstart.config.js", - "build-styleguide-quickstart": "tsc && styleguidist build --config styleguide-quickstart.config.js", - "build-docs": "./build_docs.sh", - "test": "tsc && jest --coverage", - "prettier-check": "prettier --check src/**/*.ts src/**/*.tsx", - "prettier": "prettier --write src/**/*.ts src/**/*.tsx" - }, - "dependencies": { - "@babel/runtime": "^7.1.2" - }, - "husky": { - "hooks": { - "pre-push": "npm run prettier-check" - } - } -} +{ + "name": "react-dropzone-uploader", + "version": "2.11.0", + "author": "Kyle Bebak ", + "description": "React file dropzone and uploader: fully customizable, progress indicators, upload cancellation and restart, zero deps and excellent TypeScript support", + "main": "./dist/react-dropzone-uploader.js", + "types": "./dist/Dropzone.tsx", + "keywords": [ + "react", + "react-component", + "file", + "HTML5", + "input", + "dropzone", + "uploader", + "progress", + "typescript", + "s3" + ], + "repository": { + "type": "git", + "url": "git://github.com/fortana-co/react-dropzone-uploader.git" + }, + "license": "MIT", + "peerDependencies": { + "react": ">=16.2.0", + "react-dom": ">=16.2.0", + "prop-types": ">=15.5.10" + }, + "devDependencies": { + "@babel/core": "^7.1.2", + "@babel/plugin-proposal-class-properties": "^7.1.0", + "@babel/plugin-proposal-function-bind": "^7.0.0", + "@babel/plugin-proposal-object-rest-spread": "^7.0.0", + "@babel/plugin-transform-runtime": "^7.1.0", + "@babel/preset-env": "^7.1.0", + "@babel/preset-react": "^7.0.0", + "@types/react": "^16.8.10", + "@types/react-dom": "^16.8.3", + "babel-core": "^7.0.0-bridge.0", + "babel-jest": "^23.6.0", + "babel-loader": "^8.0.4", + "babel-polyfill": "^6.26.0", + "css-loader": "^1.0.0", + "docusaurus-init": "^1.0.2", + "enzyme": "^3.7.0", + "enzyme-adapter-react-16": "^1.7.0", + "html5-file-selector": "^2.1.0", + "husky": "^1.3.1", + "jest": "^23.6.0", + "prettier": "1.16.4", + "react": ">=16.2.0", + "react-dom": ">=16.2.0", + "react-styleguidist": "^8.0.6", + "react-test-renderer": "^16.6.3", + "react-toastify": "^4.5.2", + "style-loader": "^0.23.1", + "typescript": "^3.4.1", + "uglifyjs-webpack-plugin": "^2.0.1", + "url-loader": "^1.1.2", + "webpack": "^4.21.0", + "webpack-cli": "^3.1.2", + "webpack-dev-server": "^3.1.14" + }, + "jest": { + "moduleNameMapper": { + "\\.(png|jpg|jpeg|gif|svg|woff|woff2)$": "/tests/fileMock.js" + } + }, + "scripts": { + "build": "tsc && NODE_ENV=production webpack --config webpack.build.config.js && cp src/styles.css dist/styles.css && cp src/*.ts* dist", + "dev": "tsc -w -p tsconfig.dev.json & webpack-dev-server --config webpack.config.js --mode development --open", + "styleguide": "tsc -w & styleguidist server", + "build-styleguide": "tsc && styleguidist build", + "styleguide-quickstart": "tsc -w && styleguidist server --config styleguide-quickstart.config.js", + "build-styleguide-quickstart": "tsc && styleguidist build --config styleguide-quickstart.config.js", + "build-docs": "./build_docs.sh", + "test": "tsc && jest --coverage", + "prettier-check": "prettier --check src/**/*.ts src/**/*.tsx", + "prettier": "prettier --write src/**/*.ts src/**/*.tsx" + }, + "dependencies": { + "@babel/runtime": "^7.1.2" + }, + "husky": { + "hooks": { + "pre-push": "npm run prettier-check" + } + } +} diff --git a/src/Dropzone.js b/src/Dropzone.js new file mode 100644 index 0000000..de2083a --- /dev/null +++ b/src/Dropzone.js @@ -0,0 +1,424 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import LayoutDefault from './Layout'; +import InputDefault from './Input'; +import PreviewDefault from './Preview'; +import SubmitButtonDefault from './SubmitButton'; +import { formatBytes, formatDuration, accepts, resolveValue, mergeStyles, defaultClassNames, getFilesFromEvent as defaultGetFilesFromEvent, } from './utils'; +class Dropzone extends React.Component { + constructor(props) { + super(props); + this.forceUpdate = () => { + if (this.mounted) + super.forceUpdate(); + }; + this.getFilesFromEvent = () => { + return this.props.getFilesFromEvent || defaultGetFilesFromEvent; + }; + this.getDataTransferItemsFromEvent = () => { + return this.props.getDataTransferItemsFromEvent || defaultGetFilesFromEvent; + }; + this.handleDragEnter = async (e) => { + e.preventDefault(); + e.stopPropagation(); + const dragged = (await this.getDataTransferItemsFromEvent()(e)); + this.setState({ active: true, dragged }); + }; + this.handleDragOver = async (e) => { + e.preventDefault(); + e.stopPropagation(); + clearTimeout(this.dragTimeoutId); + const dragged = await this.getDataTransferItemsFromEvent()(e); + this.setState({ active: true, dragged }); + }; + this.handleDragLeave = (e) => { + e.preventDefault(); + e.stopPropagation(); + // prevents repeated toggling of `active` state when file is dragged over children of uploader + // see: https://www.smashingmagazine.com/2018/01/drag-drop-file-uploader-vanilla-js/ + this.dragTimeoutId = window.setTimeout(() => this.setState({ active: false, dragged: [] }), 150); + }; + this.handleDrop = async (e) => { + e.preventDefault(); + e.stopPropagation(); + this.setState({ active: false, dragged: [] }); + const files = (await this.getFilesFromEvent()(e)); + this.handleFiles(files); + }; + this.handleDropDisabled = (e) => { + e.preventDefault(); + e.stopPropagation(); + this.setState({ active: false, dragged: [] }); + }; + this.handleChangeStatus = (fileWithMeta) => { + if (!this.props.onChangeStatus) + return; + const { meta = {} } = this.props.onChangeStatus(fileWithMeta, fileWithMeta.meta.status, this.files) || {}; + if (meta) { + delete meta.status; + fileWithMeta.meta = { ...fileWithMeta.meta, ...meta }; + this.forceUpdate(); + } + }; + this.handleSubmit = (files) => { + if (this.props.onSubmit) + this.props.onSubmit(files, [...this.files]); + }; + this.handleCancel = (fileWithMeta) => { + if (fileWithMeta.meta.status !== 'uploading') + return; + fileWithMeta.meta.status = 'aborted'; + if (fileWithMeta.xhr) + fileWithMeta.xhr.abort(); + this.handleChangeStatus(fileWithMeta); + this.forceUpdate(); + }; + this.handleRemove = (fileWithMeta) => { + const index = this.files.findIndex(f => f === fileWithMeta); + if (index !== -1) { + URL.revokeObjectURL(fileWithMeta.meta.previewUrl || ''); + fileWithMeta.meta.status = 'removed'; + this.handleChangeStatus(fileWithMeta); + this.files.splice(index, 1); + this.forceUpdate(); + } + }; + this.handleRestart = (fileWithMeta) => { + if (!this.props.getUploadParams) + return; + if (fileWithMeta.meta.status === 'ready') + fileWithMeta.meta.status = 'started'; + else + fileWithMeta.meta.status = 'restarted'; + this.handleChangeStatus(fileWithMeta); + fileWithMeta.meta.status = 'getting_upload_params'; + fileWithMeta.meta.percent = 0; + this.handleChangeStatus(fileWithMeta); + this.forceUpdate(); + this.uploadFile(fileWithMeta); + }; + // expects an array of File objects + this.handleFiles = (files) => { + files.forEach((f, i) => this.handleFile(f, `${new Date().getTime()}-${i}`)); + const { current } = this.dropzone; + if (current) + setTimeout(() => current.scroll({ top: current.scrollHeight, behavior: 'smooth' }), 150); + }; + this.handleFile = async (file, id) => { + const { name, size, type, lastModified } = file; + const { minSizeBytes, maxSizeBytes, maxFiles, accept, getUploadParams, autoUpload, validate } = this.props; + const uploadedDate = new Date().toISOString(); + const lastModifiedDate = lastModified && new Date(lastModified).toISOString(); + const fileWithMeta = { + file, + meta: { name, size, type, lastModifiedDate, uploadedDate, percent: 0, id }, + }; + // firefox versions prior to 53 return a bogus mime type for file drag events, + // so files with that mime type are always accepted + if (file.type !== 'application/x-moz-file' && !accepts(file, accept)) { + fileWithMeta.meta.status = 'rejected_file_type'; + this.handleChangeStatus(fileWithMeta); + return; + } + if (this.files.length >= maxFiles) { + fileWithMeta.meta.status = 'rejected_max_files'; + this.handleChangeStatus(fileWithMeta); + return; + } + fileWithMeta.cancel = () => this.handleCancel(fileWithMeta); + fileWithMeta.remove = () => this.handleRemove(fileWithMeta); + fileWithMeta.restart = () => this.handleRestart(fileWithMeta); + fileWithMeta.meta.status = 'preparing'; + this.files.push(fileWithMeta); + this.handleChangeStatus(fileWithMeta); + this.forceUpdate(); + if (size < minSizeBytes || size > maxSizeBytes) { + fileWithMeta.meta.status = 'error_file_size'; + this.handleChangeStatus(fileWithMeta); + this.forceUpdate(); + return; + } + await this.generatePreview(fileWithMeta); + if (validate) { + const error = validate(fileWithMeta); + if (error) { + fileWithMeta.meta.status = 'error_validation'; + fileWithMeta.meta.validationError = error; // usually a string, but doesn't have to be + this.handleChangeStatus(fileWithMeta); + this.forceUpdate(); + return; + } + } + if (getUploadParams) { + if (autoUpload) { + this.uploadFile(fileWithMeta); + fileWithMeta.meta.status = 'getting_upload_params'; + } + else { + fileWithMeta.meta.status = 'ready'; + } + } + else { + fileWithMeta.meta.status = 'done'; + } + this.handleChangeStatus(fileWithMeta); + this.forceUpdate(); + }; + this.generatePreview = async (fileWithMeta) => { + const { meta: { type }, file, } = fileWithMeta; + const isImage = type.startsWith('image/'); + const isAudio = type.startsWith('audio/'); + const isVideo = type.startsWith('video/'); + if (!isImage && !isAudio && !isVideo) + return; + const objectUrl = URL.createObjectURL(file); + const fileCallbackToPromise = (fileObj) => { + return Promise.race([ + new Promise(resolve => { + if (fileObj instanceof HTMLImageElement) + fileObj.onload = resolve; + else + fileObj.onloadedmetadata = resolve; + }), + new Promise((_, reject) => { + setTimeout(reject, 1000); + }), + ]); + }; + try { + if (isImage) { + const img = new Image(); + img.src = objectUrl; + fileWithMeta.meta.previewUrl = objectUrl; + await fileCallbackToPromise(img); + fileWithMeta.meta.width = img.width; + fileWithMeta.meta.height = img.height; + } + if (isAudio) { + const audio = new Audio(); + audio.src = objectUrl; + await fileCallbackToPromise(audio); + fileWithMeta.meta.duration = audio.duration; + } + if (isVideo) { + const video = document.createElement('video'); + video.src = objectUrl; + await fileCallbackToPromise(video); + fileWithMeta.meta.duration = video.duration; + fileWithMeta.meta.videoWidth = video.videoWidth; + fileWithMeta.meta.videoHeight = video.videoHeight; + } + if (!isImage) + URL.revokeObjectURL(objectUrl); + } + catch (e) { + URL.revokeObjectURL(objectUrl); + } + this.forceUpdate(); + }; + this.uploadFile = async (fileWithMeta) => { + const { getUploadParams } = this.props; + if (!getUploadParams) + return; + let params = null; + try { + params = await getUploadParams(fileWithMeta); + } + catch (e) { + console.error('Error Upload Params', e.stack); + } + if (params === null) + return; + const { url, method = 'POST', body, fields = {}, headers = {}, meta: extraMeta = {} } = params; + delete extraMeta.status; + if (!url) { + fileWithMeta.meta.status = 'error_upload_params'; + this.handleChangeStatus(fileWithMeta); + this.forceUpdate(); + return; + } + const xhr = new XMLHttpRequest(); + if (params.withCredentials) + xhr.withCredentials = true; + const formData = new FormData(); + xhr.open(method, url, true); + for (const field of Object.keys(fields)) + formData.append(field, fields[field]); + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + for (const header of Object.keys(headers)) + xhr.setRequestHeader(header, headers[header]); + fileWithMeta.meta = { ...fileWithMeta.meta, ...extraMeta }; + // update progress (can be used to show progress indicator) + xhr.upload.addEventListener('progress', e => { + fileWithMeta.meta.percent = (e.loaded * 100.0) / e.total || 100; + this.forceUpdate(); + }); + xhr.addEventListener('readystatechange', () => { + // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState + if (xhr.readyState !== 2 && xhr.readyState !== 4) + return; + if (xhr.status === 0 && fileWithMeta.meta.status !== 'aborted') { + fileWithMeta.meta.status = 'exception_upload'; + this.handleChangeStatus(fileWithMeta); + this.forceUpdate(); + } + if (xhr.status > 0 && xhr.status < 400) { + fileWithMeta.meta.percent = 100; + if (xhr.readyState === 2) + fileWithMeta.meta.status = 'headers_received'; + if (xhr.readyState === 4) + fileWithMeta.meta.status = 'done'; + this.handleChangeStatus(fileWithMeta); + this.forceUpdate(); + } + if (xhr.status >= 400 && fileWithMeta.meta.status !== 'error_upload') { + fileWithMeta.meta.status = 'error_upload'; + this.handleChangeStatus(fileWithMeta); + this.forceUpdate(); + } + }); + formData.append('file', fileWithMeta.file); + if (this.props.timeout) + xhr.timeout = this.props.timeout; + xhr.send(body || formData); + fileWithMeta.xhr = xhr; + fileWithMeta.meta.status = 'uploading'; + this.handleChangeStatus(fileWithMeta); + this.forceUpdate(); + }; + this.state = { + active: false, + dragged: [], + }; + this.files = []; + this.mounted = true; + this.dropzone = React.createRef(); + } + componentDidMount() { + if (this.props.initialFiles) + this.handleFiles(this.props.initialFiles); + } + componentDidUpdate(prevProps) { + const { initialFiles } = this.props; + if (prevProps.initialFiles !== initialFiles && initialFiles) + this.handleFiles(initialFiles); + } + componentWillUnmount() { + this.mounted = false; + for (const fileWithMeta of this.files) + this.handleCancel(fileWithMeta); + } + render() { + const { accept, multiple, maxFiles, minSizeBytes, maxSizeBytes, onSubmit, getUploadParams, disabled, canCancel, canRemove, canRestart, inputContent, inputWithFilesContent, submitButtonDisabled, submitButtonContent, classNames, styles, addClassNames, InputComponent, PreviewComponent, SubmitButtonComponent, LayoutComponent, } = this.props; + const { active, dragged } = this.state; + const reject = dragged.some(file => file.type !== 'application/x-moz-file' && !accepts(file, accept)); + const extra = { active, reject, dragged, accept, multiple, minSizeBytes, maxSizeBytes, maxFiles }; + const files = [...this.files]; + const dropzoneDisabled = resolveValue(disabled, files, extra); + const { classNames: { dropzone: dropzoneClassName, dropzoneActive: dropzoneActiveClassName, dropzoneReject: dropzoneRejectClassName, dropzoneDisabled: dropzoneDisabledClassName, input: inputClassName, inputLabel: inputLabelClassName, inputLabelWithFiles: inputLabelWithFilesClassName, preview: previewClassName, previewImage: previewImageClassName, submitButtonContainer: submitButtonContainerClassName, submitButton: submitButtonClassName, }, styles: { dropzone: dropzoneStyle, dropzoneActive: dropzoneActiveStyle, dropzoneReject: dropzoneRejectStyle, dropzoneDisabled: dropzoneDisabledStyle, input: inputStyle, inputLabel: inputLabelStyle, inputLabelWithFiles: inputLabelWithFilesStyle, preview: previewStyle, previewImage: previewImageStyle, submitButtonContainer: submitButtonContainerStyle, submitButton: submitButtonStyle, }, } = mergeStyles(classNames, styles, addClassNames, files, extra); + const Input = InputComponent || InputDefault; + const Preview = PreviewComponent || PreviewDefault; + const SubmitButton = SubmitButtonComponent || SubmitButtonDefault; + const Layout = LayoutComponent || LayoutDefault; + let previews = null; + if (PreviewComponent !== null) { + previews = files.map(f => { + return ( + //@ts-ignore + React.createElement(Preview, { className: previewClassName, imageClassName: previewImageClassName, style: previewStyle, imageStyle: previewImageStyle, key: f.meta.id, fileWithMeta: f, meta: { ...f.meta }, isUpload: Boolean(getUploadParams), canCancel: resolveValue(canCancel, files, extra), canRemove: resolveValue(canRemove, files, extra), canRestart: resolveValue(canRestart, files, extra), files: files, extra: extra })); + }); + } + const input = InputComponent !== null ? ( + //@ts-ignore + React.createElement(Input, { className: inputClassName, labelClassName: inputLabelClassName, labelWithFilesClassName: inputLabelWithFilesClassName, style: inputStyle, labelStyle: inputLabelStyle, labelWithFilesStyle: inputLabelWithFilesStyle, getFilesFromEvent: this.getFilesFromEvent(), accept: accept, multiple: multiple, disabled: dropzoneDisabled, content: resolveValue(inputContent, files, extra), withFilesContent: resolveValue(inputWithFilesContent, files, extra), onFiles: this.handleFiles, files: files, extra: extra })) : null; + const submitButton = onSubmit && SubmitButtonComponent !== null ? ( + //@ts-ignore + React.createElement(SubmitButton, { className: submitButtonContainerClassName, buttonClassName: submitButtonClassName, style: submitButtonContainerStyle, buttonStyle: submitButtonStyle, disabled: resolveValue(submitButtonDisabled, files, extra), content: resolveValue(submitButtonContent, files, extra), onSubmit: this.handleSubmit, files: files, extra: extra })) : null; + let className = dropzoneClassName; + let style = dropzoneStyle; + if (dropzoneDisabled) { + className = `${className} ${dropzoneDisabledClassName}`; + style = { ...(style || {}), ...(dropzoneDisabledStyle || {}) }; + } + else if (reject) { + className = `${className} ${dropzoneRejectClassName}`; + style = { ...(style || {}), ...(dropzoneRejectStyle || {}) }; + } + else if (active) { + className = `${className} ${dropzoneActiveClassName}`; + style = { ...(style || {}), ...(dropzoneActiveStyle || {}) }; + } + return ( + //@ts-ignore + React.createElement(Layout, { input: input, previews: previews, submitButton: submitButton, dropzoneProps: { + ref: this.dropzone, + className, + style: style, + onDragEnter: this.handleDragEnter, + onDragOver: this.handleDragOver, + onDragLeave: this.handleDragLeave, + onDrop: dropzoneDisabled ? this.handleDropDisabled : this.handleDrop, + }, files: files, extra: { + ...extra, + onFiles: this.handleFiles, + onCancelFile: this.handleCancel, + onRemoveFile: this.handleRemove, + onRestartFile: this.handleRestart, + } })); + } +} +Dropzone.defaultProps = { + accept: '*', + multiple: true, + minSizeBytes: 0, + maxSizeBytes: Number.MAX_SAFE_INTEGER, + maxFiles: Number.MAX_SAFE_INTEGER, + autoUpload: true, + disabled: false, + canCancel: true, + canRemove: true, + canRestart: true, + inputContent: 'Drag Files or Click to Browse', + inputWithFilesContent: 'Add Files', + submitButtonDisabled: false, + submitButtonContent: 'Submit', + classNames: {}, + styles: {}, + addClassNames: {}, +}; +// @ts-ignore +Dropzone.propTypes = { + onChangeStatus: PropTypes.func, + getUploadParams: PropTypes.func, + onSubmit: PropTypes.func, + getFilesFromEvent: PropTypes.func, + getDataTransferItemsFromEvent: PropTypes.func, + accept: PropTypes.string, + multiple: PropTypes.bool, + minSizeBytes: PropTypes.number.isRequired, + maxSizeBytes: PropTypes.number.isRequired, + maxFiles: PropTypes.number.isRequired, + validate: PropTypes.func, + autoUpload: PropTypes.bool, + timeout: PropTypes.number, + initialFiles: PropTypes.arrayOf(PropTypes.any), + /* component customization */ + disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), + canCancel: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), + canRemove: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), + canRestart: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), + inputContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + inputWithFilesContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + submitButtonDisabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), + submitButtonContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + classNames: PropTypes.object.isRequired, + styles: PropTypes.object.isRequired, + addClassNames: PropTypes.object.isRequired, + /* component injection */ + InputComponent: PropTypes.func, + PreviewComponent: PropTypes.func, + SubmitButtonComponent: PropTypes.func, + LayoutComponent: PropTypes.func, +}; +export default Dropzone; +export { LayoutDefault as Layout, InputDefault as Input, PreviewDefault as Preview, SubmitButtonDefault as SubmitButton, formatBytes, formatDuration, accepts, defaultClassNames, defaultGetFilesFromEvent as getFilesFromEvent, }; diff --git a/src/Dropzone.tsx b/src/Dropzone.tsx index da29a80..1ff0ef7 100644 --- a/src/Dropzone.tsx +++ b/src/Dropzone.tsx @@ -1,816 +1,818 @@ -import React from 'react' -import PropTypes from 'prop-types' - -import LayoutDefault from './Layout' -import InputDefault from './Input' -import PreviewDefault from './Preview' -import SubmitButtonDefault from './SubmitButton' -import { - formatBytes, - formatDuration, - accepts, - resolveValue, - mergeStyles, - defaultClassNames, - getFilesFromEvent as defaultGetFilesFromEvent, -} from './utils' - -export type StatusValue = - | 'rejected_file_type' - | 'rejected_max_files' - | 'preparing' - | 'error_file_size' - | 'error_validation' - | 'ready' - | 'started' - | 'getting_upload_params' - | 'error_upload_params' - | 'uploading' - | 'exception_upload' - | 'aborted' - | 'restarted' - | 'removed' - | 'error_upload' - | 'headers_received' - | 'done' - -export type MethodValue = - | 'delete' - | 'get' - | 'head' - | 'options' - | 'patch' - | 'post' - | 'put' - | 'DELETE' - | 'GET' - | 'HEAD' - | 'OPTIONS' - | 'PATCH' - | 'POST' - | 'PUT' - -export interface IMeta { - id: string - status: StatusValue - type: string // MIME type, example: `image/*` - name: string - uploadedDate: string // ISO string - percent: number - size: number // bytes - lastModifiedDate: string // ISO string - previewUrl?: string // from URL.createObjectURL - duration?: number // seconds - width?: number - height?: number - videoWidth?: number - videoHeight?: number - validationError?: any -} - -export interface IFileWithMeta { - file: File - meta: IMeta - cancel: () => void - restart: () => void - remove: () => void - xhr?: XMLHttpRequest -} - -export interface IExtra { - active: boolean - reject: boolean - dragged: DataTransferItem[] - accept: string - multiple: boolean - minSizeBytes: number - maxSizeBytes: number - maxFiles: number -} - -export interface IUploadParams { - url: string - method?: MethodValue - body?: string | FormData | ArrayBuffer | Blob | File | URLSearchParams - fields?: { [name: string]: string | Blob } - headers?: { [name: string]: string } - meta?: { [name: string]: any } -} - -export type CustomizationFunction = (allFiles: IFileWithMeta[], extra: IExtra) => T - -export interface IStyleCustomization { - dropzone?: T | CustomizationFunction - dropzoneActive?: T | CustomizationFunction - dropzoneReject?: T | CustomizationFunction - dropzoneDisabled?: T | CustomizationFunction - input?: T | CustomizationFunction - inputLabel?: T | CustomizationFunction - inputLabelWithFiles?: T | CustomizationFunction - preview?: T | CustomizationFunction - previewImage?: T | CustomizationFunction - submitButtonContainer?: T | CustomizationFunction - submitButton?: T | CustomizationFunction -} - -export interface IExtraLayout extends IExtra { - onFiles(files: File[]): void - onCancelFile(file: IFileWithMeta): void - onRemoveFile(file: IFileWithMeta): void - onRestartFile(file: IFileWithMeta): void -} - -export interface ILayoutProps { - files: IFileWithMeta[] - extra: IExtraLayout - input: React.ReactNode - previews: React.ReactNode[] | null - submitButton: React.ReactNode - dropzoneProps: { - ref: React.RefObject - className: string - style?: React.CSSProperties - onDragEnter(event: React.DragEvent): void - onDragOver(event: React.DragEvent): void - onDragLeave(event: React.DragEvent): void - onDrop(event: React.DragEvent): void - } -} - -interface ICommonProps { - files: IFileWithMeta[] - extra: IExtra -} - -export interface IPreviewProps extends ICommonProps { - meta: IMeta - className?: string - imageClassName?: string - style?: React.CSSProperties - imageStyle?: React.CSSProperties - fileWithMeta: IFileWithMeta - isUpload: boolean - canCancel: boolean - canRemove: boolean - canRestart: boolean -} - -export interface IInputProps extends ICommonProps { - className?: string - labelClassName?: string - labelWithFilesClassName?: string - style?: React.CSSProperties - labelStyle?: React.CSSProperties - labelWithFilesStyle?: React.CSSProperties - getFilesFromEvent: (event: React.ChangeEvent) => Promise - accept: string - multiple: boolean - disabled: boolean - content?: React.ReactNode - withFilesContent?: React.ReactNode - onFiles: (files: File[]) => void -} - -export interface ISubmitButtonProps extends ICommonProps { - className?: string - buttonClassName?: string - style?: React.CSSProperties - buttonStyle?: React.CSSProperties - disabled: boolean - content?: React.ReactNode - onSubmit: (files: IFileWithMeta[]) => void -} - -type ReactComponent = (props: Props) => React.ReactNode | React.Component - -export interface IDropzoneProps { - onChangeStatus?( - file: IFileWithMeta, - status: StatusValue, - allFiles: IFileWithMeta[], - ): { meta: { [name: string]: any } } | void - getUploadParams?(file: IFileWithMeta): IUploadParams | Promise - onSubmit?(successFiles: IFileWithMeta[], allFiles: IFileWithMeta[]): void - - getFilesFromEvent?: ( - event: React.DragEvent | React.ChangeEvent, - ) => Promise | File[] - getDataTransferItemsFromEvent?: ( - event: React.DragEvent, - ) => Promise | DataTransferItem[] - - accept: string - multiple: boolean - minSizeBytes: number - maxSizeBytes: number - maxFiles: number - - validate?(file: IFileWithMeta): any // usually a string, but can be anything - - autoUpload: boolean - timeout?: number - - initialFiles?: File[] - - /* component customization */ - disabled: boolean | CustomizationFunction - - canCancel: boolean | CustomizationFunction - canRemove: boolean | CustomizationFunction - canRestart: boolean | CustomizationFunction - - inputContent: React.ReactNode | CustomizationFunction - inputWithFilesContent: React.ReactNode | CustomizationFunction - - submitButtonDisabled: boolean | CustomizationFunction - submitButtonContent: React.ReactNode | CustomizationFunction - - classNames: IStyleCustomization - styles: IStyleCustomization - addClassNames: IStyleCustomization - - /* component injection */ - LayoutComponent?: ReactComponent - PreviewComponent?: ReactComponent - InputComponent?: ReactComponent - SubmitButtonComponent?: ReactComponent -} - -class Dropzone extends React.Component { - static defaultProps: IDropzoneProps - protected files: IFileWithMeta[] - protected mounted: boolean - protected dropzone: React.RefObject - protected dragTimeoutId?: number - - constructor(props: IDropzoneProps) { - super(props) - this.state = { - active: false, - dragged: [], - } - this.files = [] - this.mounted = true - this.dropzone = React.createRef() - } - - componentDidMount() { - if (this.props.initialFiles) this.handleFiles(this.props.initialFiles) - } - - componentDidUpdate(prevProps: IDropzoneProps) { - const { initialFiles } = this.props - if (prevProps.initialFiles !== initialFiles && initialFiles) this.handleFiles(initialFiles) - } - - componentWillUnmount() { - this.mounted = false - for (const fileWithMeta of this.files) this.handleCancel(fileWithMeta) - } - - forceUpdate = () => { - if (this.mounted) super.forceUpdate() - } - - getFilesFromEvent = () => { - return this.props.getFilesFromEvent || defaultGetFilesFromEvent - } - - getDataTransferItemsFromEvent = () => { - return this.props.getDataTransferItemsFromEvent || defaultGetFilesFromEvent - } - - handleDragEnter = async (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - const dragged = (await this.getDataTransferItemsFromEvent()(e)) as DataTransferItem[] - this.setState({ active: true, dragged }) - } - - handleDragOver = async (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - clearTimeout(this.dragTimeoutId) - const dragged = await this.getDataTransferItemsFromEvent()(e) - this.setState({ active: true, dragged }) - } - - handleDragLeave = (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - // prevents repeated toggling of `active` state when file is dragged over children of uploader - // see: https://www.smashingmagazine.com/2018/01/drag-drop-file-uploader-vanilla-js/ - this.dragTimeoutId = window.setTimeout(() => this.setState({ active: false, dragged: [] }), 150) - } - - handleDrop = async (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - this.setState({ active: false, dragged: [] }) - const files = (await this.getFilesFromEvent()(e)) as File[] - this.handleFiles(files) - } - - handleDropDisabled = (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - this.setState({ active: false, dragged: [] }) - } - - handleChangeStatus = (fileWithMeta: IFileWithMeta) => { - if (!this.props.onChangeStatus) return - const { meta = {} } = this.props.onChangeStatus(fileWithMeta, fileWithMeta.meta.status, this.files) || {} - if (meta) { - delete meta.status - fileWithMeta.meta = { ...fileWithMeta.meta, ...meta } - this.forceUpdate() - } - } - - handleSubmit = (files: IFileWithMeta[]) => { - if (this.props.onSubmit) this.props.onSubmit(files, [...this.files]) - } - - handleCancel = (fileWithMeta: IFileWithMeta) => { - if (fileWithMeta.meta.status !== 'uploading') return - fileWithMeta.meta.status = 'aborted' - if (fileWithMeta.xhr) fileWithMeta.xhr.abort() - this.handleChangeStatus(fileWithMeta) - this.forceUpdate() - } - - handleRemove = (fileWithMeta: IFileWithMeta) => { - const index = this.files.findIndex(f => f === fileWithMeta) - if (index !== -1) { - URL.revokeObjectURL(fileWithMeta.meta.previewUrl || '') - fileWithMeta.meta.status = 'removed' - this.handleChangeStatus(fileWithMeta) - this.files.splice(index, 1) - this.forceUpdate() - } - } - - handleRestart = (fileWithMeta: IFileWithMeta) => { - if (!this.props.getUploadParams) return - - if (fileWithMeta.meta.status === 'ready') fileWithMeta.meta.status = 'started' - else fileWithMeta.meta.status = 'restarted' - this.handleChangeStatus(fileWithMeta) - - fileWithMeta.meta.status = 'getting_upload_params' - fileWithMeta.meta.percent = 0 - this.handleChangeStatus(fileWithMeta) - this.forceUpdate() - this.uploadFile(fileWithMeta) - } - - // expects an array of File objects - handleFiles = (files: File[]) => { - files.forEach((f, i) => this.handleFile(f, `${new Date().getTime()}-${i}`)) - const { current } = this.dropzone - if (current) setTimeout(() => current.scroll({ top: current.scrollHeight, behavior: 'smooth' }), 150) - } - - handleFile = async (file: File, id: string) => { - const { name, size, type, lastModified } = file - const { minSizeBytes, maxSizeBytes, maxFiles, accept, getUploadParams, autoUpload, validate } = this.props - - const uploadedDate = new Date().toISOString() - const lastModifiedDate = lastModified && new Date(lastModified).toISOString() - const fileWithMeta = { - file, - meta: { name, size, type, lastModifiedDate, uploadedDate, percent: 0, id }, - } as IFileWithMeta - - // firefox versions prior to 53 return a bogus mime type for file drag events, - // so files with that mime type are always accepted - if (file.type !== 'application/x-moz-file' && !accepts(file, accept)) { - fileWithMeta.meta.status = 'rejected_file_type' - this.handleChangeStatus(fileWithMeta) - return - } - if (this.files.length >= maxFiles) { - fileWithMeta.meta.status = 'rejected_max_files' - this.handleChangeStatus(fileWithMeta) - return - } - - fileWithMeta.cancel = () => this.handleCancel(fileWithMeta) - fileWithMeta.remove = () => this.handleRemove(fileWithMeta) - fileWithMeta.restart = () => this.handleRestart(fileWithMeta) - - fileWithMeta.meta.status = 'preparing' - this.files.push(fileWithMeta) - this.handleChangeStatus(fileWithMeta) - this.forceUpdate() - - if (size < minSizeBytes || size > maxSizeBytes) { - fileWithMeta.meta.status = 'error_file_size' - this.handleChangeStatus(fileWithMeta) - this.forceUpdate() - return - } - - await this.generatePreview(fileWithMeta) - - if (validate) { - const error = validate(fileWithMeta) - if (error) { - fileWithMeta.meta.status = 'error_validation' - fileWithMeta.meta.validationError = error // usually a string, but doesn't have to be - this.handleChangeStatus(fileWithMeta) - this.forceUpdate() - return - } - } - - if (getUploadParams) { - if (autoUpload) { - this.uploadFile(fileWithMeta) - fileWithMeta.meta.status = 'getting_upload_params' - } else { - fileWithMeta.meta.status = 'ready' - } - } else { - fileWithMeta.meta.status = 'done' - } - this.handleChangeStatus(fileWithMeta) - this.forceUpdate() - } - - generatePreview = async (fileWithMeta: IFileWithMeta) => { - const { - meta: { type }, - file, - } = fileWithMeta - const isImage = type.startsWith('image/') - const isAudio = type.startsWith('audio/') - const isVideo = type.startsWith('video/') - if (!isImage && !isAudio && !isVideo) return - - const objectUrl = URL.createObjectURL(file) - - const fileCallbackToPromise = (fileObj: HTMLImageElement | HTMLAudioElement) => { - return Promise.race([ - new Promise(resolve => { - if (fileObj instanceof HTMLImageElement) fileObj.onload = resolve - else fileObj.onloadedmetadata = resolve - }), - new Promise((_, reject) => { - setTimeout(reject, 1000) - }), - ]) - } - - try { - if (isImage) { - const img = new Image() - img.src = objectUrl - fileWithMeta.meta.previewUrl = objectUrl - await fileCallbackToPromise(img) - fileWithMeta.meta.width = img.width - fileWithMeta.meta.height = img.height - } - - if (isAudio) { - const audio = new Audio() - audio.src = objectUrl - await fileCallbackToPromise(audio) - fileWithMeta.meta.duration = audio.duration - } - - if (isVideo) { - const video = document.createElement('video') - video.src = objectUrl - await fileCallbackToPromise(video) - fileWithMeta.meta.duration = video.duration - fileWithMeta.meta.videoWidth = video.videoWidth - fileWithMeta.meta.videoHeight = video.videoHeight - } - if (!isImage) URL.revokeObjectURL(objectUrl) - } catch (e) { - URL.revokeObjectURL(objectUrl) - } - this.forceUpdate() - } - - uploadFile = async (fileWithMeta: IFileWithMeta) => { - const { getUploadParams } = this.props - if (!getUploadParams) return - let params: IUploadParams | null = null - try { - params = await getUploadParams(fileWithMeta) - } catch (e) { - console.error('Error Upload Params', e.stack) - } - if (params === null) return - const { url, method = 'POST', body, fields = {}, headers = {}, meta: extraMeta = {} } = params - delete extraMeta.status - - if (!url) { - fileWithMeta.meta.status = 'error_upload_params' - this.handleChangeStatus(fileWithMeta) - this.forceUpdate() - return - } - - const xhr = new XMLHttpRequest() - const formData = new FormData() - xhr.open(method, url, true) - - for (const field of Object.keys(fields)) formData.append(field, fields[field]) - xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest') - for (const header of Object.keys(headers)) xhr.setRequestHeader(header, headers[header]) - fileWithMeta.meta = { ...fileWithMeta.meta, ...extraMeta } - - // update progress (can be used to show progress indicator) - xhr.upload.addEventListener('progress', e => { - fileWithMeta.meta.percent = (e.loaded * 100.0) / e.total || 100 - this.forceUpdate() - }) - - xhr.addEventListener('readystatechange', () => { - // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState - if (xhr.readyState !== 2 && xhr.readyState !== 4) return - - if (xhr.status === 0 && fileWithMeta.meta.status !== 'aborted') { - fileWithMeta.meta.status = 'exception_upload' - this.handleChangeStatus(fileWithMeta) - this.forceUpdate() - } - - if (xhr.status > 0 && xhr.status < 400) { - fileWithMeta.meta.percent = 100 - if (xhr.readyState === 2) fileWithMeta.meta.status = 'headers_received' - if (xhr.readyState === 4) fileWithMeta.meta.status = 'done' - this.handleChangeStatus(fileWithMeta) - this.forceUpdate() - } - - if (xhr.status >= 400 && fileWithMeta.meta.status !== 'error_upload') { - fileWithMeta.meta.status = 'error_upload' - this.handleChangeStatus(fileWithMeta) - this.forceUpdate() - } - }) - - formData.append('file', fileWithMeta.file) - if (this.props.timeout) xhr.timeout = this.props.timeout - xhr.send(body || formData) - fileWithMeta.xhr = xhr - fileWithMeta.meta.status = 'uploading' - this.handleChangeStatus(fileWithMeta) - this.forceUpdate() - } - - render() { - const { - accept, - multiple, - maxFiles, - minSizeBytes, - maxSizeBytes, - onSubmit, - getUploadParams, - disabled, - canCancel, - canRemove, - canRestart, - inputContent, - inputWithFilesContent, - submitButtonDisabled, - submitButtonContent, - classNames, - styles, - addClassNames, - InputComponent, - PreviewComponent, - SubmitButtonComponent, - LayoutComponent, - } = this.props - - const { active, dragged } = this.state - - const reject = dragged.some(file => file.type !== 'application/x-moz-file' && !accepts(file as File, accept)) - const extra = { active, reject, dragged, accept, multiple, minSizeBytes, maxSizeBytes, maxFiles } as IExtra - const files = [...this.files] - const dropzoneDisabled = resolveValue(disabled, files, extra) - - const { - classNames: { - dropzone: dropzoneClassName, - dropzoneActive: dropzoneActiveClassName, - dropzoneReject: dropzoneRejectClassName, - dropzoneDisabled: dropzoneDisabledClassName, - input: inputClassName, - inputLabel: inputLabelClassName, - inputLabelWithFiles: inputLabelWithFilesClassName, - preview: previewClassName, - previewImage: previewImageClassName, - submitButtonContainer: submitButtonContainerClassName, - submitButton: submitButtonClassName, - }, - styles: { - dropzone: dropzoneStyle, - dropzoneActive: dropzoneActiveStyle, - dropzoneReject: dropzoneRejectStyle, - dropzoneDisabled: dropzoneDisabledStyle, - input: inputStyle, - inputLabel: inputLabelStyle, - inputLabelWithFiles: inputLabelWithFilesStyle, - preview: previewStyle, - previewImage: previewImageStyle, - submitButtonContainer: submitButtonContainerStyle, - submitButton: submitButtonStyle, - }, - } = mergeStyles(classNames, styles, addClassNames, files, extra) - - const Input = InputComponent || InputDefault - const Preview = PreviewComponent || PreviewDefault - const SubmitButton = SubmitButtonComponent || SubmitButtonDefault - const Layout = LayoutComponent || LayoutDefault - - let previews = null - if (PreviewComponent !== null) { - previews = files.map(f => { - return ( - //@ts-ignore - - ) - }) - } - - const input = - InputComponent !== null ? ( - //@ts-ignore - - ) : null - - const submitButton = - onSubmit && SubmitButtonComponent !== null ? ( - //@ts-ignore - - ) : null - - let className = dropzoneClassName - let style = dropzoneStyle - - if (dropzoneDisabled) { - className = `${className} ${dropzoneDisabledClassName}` - style = { ...(style || {}), ...(dropzoneDisabledStyle || {}) } - } else if (reject) { - className = `${className} ${dropzoneRejectClassName}` - style = { ...(style || {}), ...(dropzoneRejectStyle || {}) } - } else if (active) { - className = `${className} ${dropzoneActiveClassName}` - style = { ...(style || {}), ...(dropzoneActiveStyle || {}) } - } - - return ( - //@ts-ignore - - ) - } -} - -Dropzone.defaultProps = { - accept: '*', - multiple: true, - minSizeBytes: 0, - maxSizeBytes: Number.MAX_SAFE_INTEGER, - maxFiles: Number.MAX_SAFE_INTEGER, - autoUpload: true, - disabled: false, - canCancel: true, - canRemove: true, - canRestart: true, - inputContent: 'Drag Files or Click to Browse', - inputWithFilesContent: 'Add Files', - submitButtonDisabled: false, - submitButtonContent: 'Submit', - classNames: {}, - styles: {}, - addClassNames: {}, -} - -// @ts-ignore -Dropzone.propTypes = { - onChangeStatus: PropTypes.func, - getUploadParams: PropTypes.func, - onSubmit: PropTypes.func, - - getFilesFromEvent: PropTypes.func, - getDataTransferItemsFromEvent: PropTypes.func, - - accept: PropTypes.string, - multiple: PropTypes.bool, - minSizeBytes: PropTypes.number.isRequired, - maxSizeBytes: PropTypes.number.isRequired, - maxFiles: PropTypes.number.isRequired, - - validate: PropTypes.func, - - autoUpload: PropTypes.bool, - timeout: PropTypes.number, - - initialFiles: PropTypes.arrayOf(PropTypes.any), - - /* component customization */ - disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), - - canCancel: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), - canRemove: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), - canRestart: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), - - inputContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), - inputWithFilesContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), - - submitButtonDisabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), - submitButtonContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), - - classNames: PropTypes.object.isRequired, - styles: PropTypes.object.isRequired, - addClassNames: PropTypes.object.isRequired, - - /* component injection */ - InputComponent: PropTypes.func, - PreviewComponent: PropTypes.func, - SubmitButtonComponent: PropTypes.func, - LayoutComponent: PropTypes.func, -} - -export default Dropzone -export { - LayoutDefault as Layout, - InputDefault as Input, - PreviewDefault as Preview, - SubmitButtonDefault as SubmitButton, - formatBytes, - formatDuration, - accepts, - defaultClassNames, - defaultGetFilesFromEvent as getFilesFromEvent, -} +import React from 'react' +import PropTypes from 'prop-types' + +import LayoutDefault from './Layout' +import InputDefault from './Input' +import PreviewDefault from './Preview' +import SubmitButtonDefault from './SubmitButton' +import { + formatBytes, + formatDuration, + accepts, + resolveValue, + mergeStyles, + defaultClassNames, + getFilesFromEvent as defaultGetFilesFromEvent, +} from './utils' + +export type StatusValue = + | 'rejected_file_type' + | 'rejected_max_files' + | 'preparing' + | 'error_file_size' + | 'error_validation' + | 'ready' + | 'started' + | 'getting_upload_params' + | 'error_upload_params' + | 'uploading' + | 'exception_upload' + | 'aborted' + | 'restarted' + | 'removed' + | 'error_upload' + | 'headers_received' + | 'done' + +export type MethodValue = + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'DELETE' + | 'GET' + | 'HEAD' + | 'OPTIONS' + | 'PATCH' + | 'POST' + | 'PUT' + +export interface IMeta { + id: string + status: StatusValue + type: string // MIME type, example: `image/*` + name: string + uploadedDate: string // ISO string + percent: number + size: number // bytes + lastModifiedDate: string // ISO string + previewUrl?: string // from URL.createObjectURL + duration?: number // seconds + width?: number + height?: number + videoWidth?: number + videoHeight?: number + validationError?: any +} + +export interface IFileWithMeta { + file: File + meta: IMeta + cancel: () => void + restart: () => void + remove: () => void + xhr?: XMLHttpRequest +} + +export interface IExtra { + active: boolean + reject: boolean + dragged: DataTransferItem[] + accept: string + multiple: boolean + minSizeBytes: number + maxSizeBytes: number + maxFiles: number +} + +export interface IUploadParams { + url: string + method?: MethodValue + body?: string | FormData | ArrayBuffer | Blob | File | URLSearchParams + fields?: { [name: string]: string | Blob } + headers?: { [name: string]: string } + meta?: { [name: string]: any } + withCredentials?: boolean +} + +export type CustomizationFunction = (allFiles: IFileWithMeta[], extra: IExtra) => T + +export interface IStyleCustomization { + dropzone?: T | CustomizationFunction + dropzoneActive?: T | CustomizationFunction + dropzoneReject?: T | CustomizationFunction + dropzoneDisabled?: T | CustomizationFunction + input?: T | CustomizationFunction + inputLabel?: T | CustomizationFunction + inputLabelWithFiles?: T | CustomizationFunction + preview?: T | CustomizationFunction + previewImage?: T | CustomizationFunction + submitButtonContainer?: T | CustomizationFunction + submitButton?: T | CustomizationFunction +} + +export interface IExtraLayout extends IExtra { + onFiles(files: File[]): void + onCancelFile(file: IFileWithMeta): void + onRemoveFile(file: IFileWithMeta): void + onRestartFile(file: IFileWithMeta): void +} + +export interface ILayoutProps { + files: IFileWithMeta[] + extra: IExtraLayout + input: React.ReactNode + previews: React.ReactNode[] | null + submitButton: React.ReactNode + dropzoneProps: { + ref: React.RefObject + className: string + style?: React.CSSProperties + onDragEnter(event: React.DragEvent): void + onDragOver(event: React.DragEvent): void + onDragLeave(event: React.DragEvent): void + onDrop(event: React.DragEvent): void + } +} + +interface ICommonProps { + files: IFileWithMeta[] + extra: IExtra +} + +export interface IPreviewProps extends ICommonProps { + meta: IMeta + className?: string + imageClassName?: string + style?: React.CSSProperties + imageStyle?: React.CSSProperties + fileWithMeta: IFileWithMeta + isUpload: boolean + canCancel: boolean + canRemove: boolean + canRestart: boolean +} + +export interface IInputProps extends ICommonProps { + className?: string + labelClassName?: string + labelWithFilesClassName?: string + style?: React.CSSProperties + labelStyle?: React.CSSProperties + labelWithFilesStyle?: React.CSSProperties + getFilesFromEvent: (event: React.ChangeEvent) => Promise + accept: string + multiple: boolean + disabled: boolean + content?: React.ReactNode + withFilesContent?: React.ReactNode + onFiles: (files: File[]) => void +} + +export interface ISubmitButtonProps extends ICommonProps { + className?: string + buttonClassName?: string + style?: React.CSSProperties + buttonStyle?: React.CSSProperties + disabled: boolean + content?: React.ReactNode + onSubmit: (files: IFileWithMeta[]) => void +} + +type ReactComponent = (props: Props) => React.ReactNode | React.Component + +export interface IDropzoneProps { + onChangeStatus?( + file: IFileWithMeta, + status: StatusValue, + allFiles: IFileWithMeta[], + ): { meta: { [name: string]: any } } | void + getUploadParams?(file: IFileWithMeta): IUploadParams | Promise + onSubmit?(successFiles: IFileWithMeta[], allFiles: IFileWithMeta[]): void + + getFilesFromEvent?: ( + event: React.DragEvent | React.ChangeEvent, + ) => Promise | File[] + getDataTransferItemsFromEvent?: ( + event: React.DragEvent, + ) => Promise | DataTransferItem[] + + accept: string + multiple: boolean + minSizeBytes: number + maxSizeBytes: number + maxFiles: number + + validate?(file: IFileWithMeta): any // usually a string, but can be anything + + autoUpload: boolean + timeout?: number + + initialFiles?: File[] + + /* component customization */ + disabled: boolean | CustomizationFunction + + canCancel: boolean | CustomizationFunction + canRemove: boolean | CustomizationFunction + canRestart: boolean | CustomizationFunction + + inputContent: React.ReactNode | CustomizationFunction + inputWithFilesContent: React.ReactNode | CustomizationFunction + + submitButtonDisabled: boolean | CustomizationFunction + submitButtonContent: React.ReactNode | CustomizationFunction + + classNames: IStyleCustomization + styles: IStyleCustomization + addClassNames: IStyleCustomization + + /* component injection */ + LayoutComponent?: ReactComponent + PreviewComponent?: ReactComponent + InputComponent?: ReactComponent + SubmitButtonComponent?: ReactComponent +} + +class Dropzone extends React.Component { + static defaultProps: IDropzoneProps + protected files: IFileWithMeta[] + protected mounted: boolean + protected dropzone: React.RefObject + protected dragTimeoutId?: number + + constructor(props: IDropzoneProps) { + super(props) + this.state = { + active: false, + dragged: [], + } + this.files = [] + this.mounted = true + this.dropzone = React.createRef() + } + + componentDidMount() { + if (this.props.initialFiles) this.handleFiles(this.props.initialFiles) + } + + componentDidUpdate(prevProps: IDropzoneProps) { + const { initialFiles } = this.props + if (prevProps.initialFiles !== initialFiles && initialFiles) this.handleFiles(initialFiles) + } + + componentWillUnmount() { + this.mounted = false + for (const fileWithMeta of this.files) this.handleCancel(fileWithMeta) + } + + forceUpdate = () => { + if (this.mounted) super.forceUpdate() + } + + getFilesFromEvent = () => { + return this.props.getFilesFromEvent || defaultGetFilesFromEvent + } + + getDataTransferItemsFromEvent = () => { + return this.props.getDataTransferItemsFromEvent || defaultGetFilesFromEvent + } + + handleDragEnter = async (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + const dragged = (await this.getDataTransferItemsFromEvent()(e)) as DataTransferItem[] + this.setState({ active: true, dragged }) + } + + handleDragOver = async (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + clearTimeout(this.dragTimeoutId) + const dragged = await this.getDataTransferItemsFromEvent()(e) + this.setState({ active: true, dragged }) + } + + handleDragLeave = (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + // prevents repeated toggling of `active` state when file is dragged over children of uploader + // see: https://www.smashingmagazine.com/2018/01/drag-drop-file-uploader-vanilla-js/ + this.dragTimeoutId = window.setTimeout(() => this.setState({ active: false, dragged: [] }), 150) + } + + handleDrop = async (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + this.setState({ active: false, dragged: [] }) + const files = (await this.getFilesFromEvent()(e)) as File[] + this.handleFiles(files) + } + + handleDropDisabled = (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + this.setState({ active: false, dragged: [] }) + } + + handleChangeStatus = (fileWithMeta: IFileWithMeta) => { + if (!this.props.onChangeStatus) return + const { meta = {} } = this.props.onChangeStatus(fileWithMeta, fileWithMeta.meta.status, this.files) || {} + if (meta) { + delete meta.status + fileWithMeta.meta = { ...fileWithMeta.meta, ...meta } + this.forceUpdate() + } + } + + handleSubmit = (files: IFileWithMeta[]) => { + if (this.props.onSubmit) this.props.onSubmit(files, [...this.files]) + } + + handleCancel = (fileWithMeta: IFileWithMeta) => { + if (fileWithMeta.meta.status !== 'uploading') return + fileWithMeta.meta.status = 'aborted' + if (fileWithMeta.xhr) fileWithMeta.xhr.abort() + this.handleChangeStatus(fileWithMeta) + this.forceUpdate() + } + + handleRemove = (fileWithMeta: IFileWithMeta) => { + const index = this.files.findIndex(f => f === fileWithMeta) + if (index !== -1) { + URL.revokeObjectURL(fileWithMeta.meta.previewUrl || '') + fileWithMeta.meta.status = 'removed' + this.handleChangeStatus(fileWithMeta) + this.files.splice(index, 1) + this.forceUpdate() + } + } + + handleRestart = (fileWithMeta: IFileWithMeta) => { + if (!this.props.getUploadParams) return + + if (fileWithMeta.meta.status === 'ready') fileWithMeta.meta.status = 'started' + else fileWithMeta.meta.status = 'restarted' + this.handleChangeStatus(fileWithMeta) + + fileWithMeta.meta.status = 'getting_upload_params' + fileWithMeta.meta.percent = 0 + this.handleChangeStatus(fileWithMeta) + this.forceUpdate() + this.uploadFile(fileWithMeta) + } + + // expects an array of File objects + handleFiles = (files: File[]) => { + files.forEach((f, i) => this.handleFile(f, `${new Date().getTime()}-${i}`)) + const { current } = this.dropzone + if (current) setTimeout(() => current.scroll({ top: current.scrollHeight, behavior: 'smooth' }), 150) + } + + handleFile = async (file: File, id: string) => { + const { name, size, type, lastModified } = file + const { minSizeBytes, maxSizeBytes, maxFiles, accept, getUploadParams, autoUpload, validate } = this.props + + const uploadedDate = new Date().toISOString() + const lastModifiedDate = lastModified && new Date(lastModified).toISOString() + const fileWithMeta = { + file, + meta: { name, size, type, lastModifiedDate, uploadedDate, percent: 0, id }, + } as IFileWithMeta + + // firefox versions prior to 53 return a bogus mime type for file drag events, + // so files with that mime type are always accepted + if (file.type !== 'application/x-moz-file' && !accepts(file, accept)) { + fileWithMeta.meta.status = 'rejected_file_type' + this.handleChangeStatus(fileWithMeta) + return + } + if (this.files.length >= maxFiles) { + fileWithMeta.meta.status = 'rejected_max_files' + this.handleChangeStatus(fileWithMeta) + return + } + + fileWithMeta.cancel = () => this.handleCancel(fileWithMeta) + fileWithMeta.remove = () => this.handleRemove(fileWithMeta) + fileWithMeta.restart = () => this.handleRestart(fileWithMeta) + + fileWithMeta.meta.status = 'preparing' + this.files.push(fileWithMeta) + this.handleChangeStatus(fileWithMeta) + this.forceUpdate() + + if (size < minSizeBytes || size > maxSizeBytes) { + fileWithMeta.meta.status = 'error_file_size' + this.handleChangeStatus(fileWithMeta) + this.forceUpdate() + return + } + + await this.generatePreview(fileWithMeta) + + if (validate) { + const error = validate(fileWithMeta) + if (error) { + fileWithMeta.meta.status = 'error_validation' + fileWithMeta.meta.validationError = error // usually a string, but doesn't have to be + this.handleChangeStatus(fileWithMeta) + this.forceUpdate() + return + } + } + + if (getUploadParams) { + if (autoUpload) { + this.uploadFile(fileWithMeta) + fileWithMeta.meta.status = 'getting_upload_params' + } else { + fileWithMeta.meta.status = 'ready' + } + } else { + fileWithMeta.meta.status = 'done' + } + this.handleChangeStatus(fileWithMeta) + this.forceUpdate() + } + + generatePreview = async (fileWithMeta: IFileWithMeta) => { + const { + meta: { type }, + file, + } = fileWithMeta + const isImage = type.startsWith('image/') + const isAudio = type.startsWith('audio/') + const isVideo = type.startsWith('video/') + if (!isImage && !isAudio && !isVideo) return + + const objectUrl = URL.createObjectURL(file) + + const fileCallbackToPromise = (fileObj: HTMLImageElement | HTMLAudioElement) => { + return Promise.race([ + new Promise(resolve => { + if (fileObj instanceof HTMLImageElement) fileObj.onload = resolve + else fileObj.onloadedmetadata = resolve + }), + new Promise((_, reject) => { + setTimeout(reject, 1000) + }), + ]) + } + + try { + if (isImage) { + const img = new Image() + img.src = objectUrl + fileWithMeta.meta.previewUrl = objectUrl + await fileCallbackToPromise(img) + fileWithMeta.meta.width = img.width + fileWithMeta.meta.height = img.height + } + + if (isAudio) { + const audio = new Audio() + audio.src = objectUrl + await fileCallbackToPromise(audio) + fileWithMeta.meta.duration = audio.duration + } + + if (isVideo) { + const video = document.createElement('video') + video.src = objectUrl + await fileCallbackToPromise(video) + fileWithMeta.meta.duration = video.duration + fileWithMeta.meta.videoWidth = video.videoWidth + fileWithMeta.meta.videoHeight = video.videoHeight + } + if (!isImage) URL.revokeObjectURL(objectUrl) + } catch (e) { + URL.revokeObjectURL(objectUrl) + } + this.forceUpdate() + } + + uploadFile = async (fileWithMeta: IFileWithMeta) => { + const { getUploadParams } = this.props + if (!getUploadParams) return + let params: IUploadParams | null = null + try { + params = await getUploadParams(fileWithMeta) + } catch (e) { + console.error('Error Upload Params', e.stack) + } + if (params === null) return + const { url, method = 'POST', body, fields = {}, headers = {}, meta: extraMeta = {} } = params + delete extraMeta.status + + if (!url) { + fileWithMeta.meta.status = 'error_upload_params' + this.handleChangeStatus(fileWithMeta) + this.forceUpdate() + return + } + + const xhr = new XMLHttpRequest() + if (params.withCredentials) xhr.withCredentials = true + const formData = new FormData() + xhr.open(method, url, true) + + for (const field of Object.keys(fields)) formData.append(field, fields[field]) + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest') + for (const header of Object.keys(headers)) xhr.setRequestHeader(header, headers[header]) + fileWithMeta.meta = { ...fileWithMeta.meta, ...extraMeta } + + // update progress (can be used to show progress indicator) + xhr.upload.addEventListener('progress', e => { + fileWithMeta.meta.percent = (e.loaded * 100.0) / e.total || 100 + this.forceUpdate() + }) + + xhr.addEventListener('readystatechange', () => { + // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState + if (xhr.readyState !== 2 && xhr.readyState !== 4) return + + if (xhr.status === 0 && fileWithMeta.meta.status !== 'aborted') { + fileWithMeta.meta.status = 'exception_upload' + this.handleChangeStatus(fileWithMeta) + this.forceUpdate() + } + + if (xhr.status > 0 && xhr.status < 400) { + fileWithMeta.meta.percent = 100 + if (xhr.readyState === 2) fileWithMeta.meta.status = 'headers_received' + if (xhr.readyState === 4) fileWithMeta.meta.status = 'done' + this.handleChangeStatus(fileWithMeta) + this.forceUpdate() + } + + if (xhr.status >= 400 && fileWithMeta.meta.status !== 'error_upload') { + fileWithMeta.meta.status = 'error_upload' + this.handleChangeStatus(fileWithMeta) + this.forceUpdate() + } + }) + + formData.append('file', fileWithMeta.file) + if (this.props.timeout) xhr.timeout = this.props.timeout + xhr.send(body || formData) + fileWithMeta.xhr = xhr + fileWithMeta.meta.status = 'uploading' + this.handleChangeStatus(fileWithMeta) + this.forceUpdate() + } + + render() { + const { + accept, + multiple, + maxFiles, + minSizeBytes, + maxSizeBytes, + onSubmit, + getUploadParams, + disabled, + canCancel, + canRemove, + canRestart, + inputContent, + inputWithFilesContent, + submitButtonDisabled, + submitButtonContent, + classNames, + styles, + addClassNames, + InputComponent, + PreviewComponent, + SubmitButtonComponent, + LayoutComponent, + } = this.props + + const { active, dragged } = this.state + + const reject = dragged.some(file => file.type !== 'application/x-moz-file' && !accepts(file as File, accept)) + const extra = { active, reject, dragged, accept, multiple, minSizeBytes, maxSizeBytes, maxFiles } as IExtra + const files = [...this.files] + const dropzoneDisabled = resolveValue(disabled, files, extra) + + const { + classNames: { + dropzone: dropzoneClassName, + dropzoneActive: dropzoneActiveClassName, + dropzoneReject: dropzoneRejectClassName, + dropzoneDisabled: dropzoneDisabledClassName, + input: inputClassName, + inputLabel: inputLabelClassName, + inputLabelWithFiles: inputLabelWithFilesClassName, + preview: previewClassName, + previewImage: previewImageClassName, + submitButtonContainer: submitButtonContainerClassName, + submitButton: submitButtonClassName, + }, + styles: { + dropzone: dropzoneStyle, + dropzoneActive: dropzoneActiveStyle, + dropzoneReject: dropzoneRejectStyle, + dropzoneDisabled: dropzoneDisabledStyle, + input: inputStyle, + inputLabel: inputLabelStyle, + inputLabelWithFiles: inputLabelWithFilesStyle, + preview: previewStyle, + previewImage: previewImageStyle, + submitButtonContainer: submitButtonContainerStyle, + submitButton: submitButtonStyle, + }, + } = mergeStyles(classNames, styles, addClassNames, files, extra) + + const Input = InputComponent || InputDefault + const Preview = PreviewComponent || PreviewDefault + const SubmitButton = SubmitButtonComponent || SubmitButtonDefault + const Layout = LayoutComponent || LayoutDefault + + let previews = null + if (PreviewComponent !== null) { + previews = files.map(f => { + return ( + //@ts-ignore + + ) + }) + } + + const input = + InputComponent !== null ? ( + //@ts-ignore + + ) : null + + const submitButton = + onSubmit && SubmitButtonComponent !== null ? ( + //@ts-ignore + + ) : null + + let className = dropzoneClassName + let style = dropzoneStyle + + if (dropzoneDisabled) { + className = `${className} ${dropzoneDisabledClassName}` + style = { ...(style || {}), ...(dropzoneDisabledStyle || {}) } + } else if (reject) { + className = `${className} ${dropzoneRejectClassName}` + style = { ...(style || {}), ...(dropzoneRejectStyle || {}) } + } else if (active) { + className = `${className} ${dropzoneActiveClassName}` + style = { ...(style || {}), ...(dropzoneActiveStyle || {}) } + } + + return ( + //@ts-ignore + + ) + } +} + +Dropzone.defaultProps = { + accept: '*', + multiple: true, + minSizeBytes: 0, + maxSizeBytes: Number.MAX_SAFE_INTEGER, + maxFiles: Number.MAX_SAFE_INTEGER, + autoUpload: true, + disabled: false, + canCancel: true, + canRemove: true, + canRestart: true, + inputContent: 'Drag Files or Click to Browse', + inputWithFilesContent: 'Add Files', + submitButtonDisabled: false, + submitButtonContent: 'Submit', + classNames: {}, + styles: {}, + addClassNames: {}, +} + +// @ts-ignore +Dropzone.propTypes = { + onChangeStatus: PropTypes.func, + getUploadParams: PropTypes.func, + onSubmit: PropTypes.func, + + getFilesFromEvent: PropTypes.func, + getDataTransferItemsFromEvent: PropTypes.func, + + accept: PropTypes.string, + multiple: PropTypes.bool, + minSizeBytes: PropTypes.number.isRequired, + maxSizeBytes: PropTypes.number.isRequired, + maxFiles: PropTypes.number.isRequired, + + validate: PropTypes.func, + + autoUpload: PropTypes.bool, + timeout: PropTypes.number, + + initialFiles: PropTypes.arrayOf(PropTypes.any), + + /* component customization */ + disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), + + canCancel: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), + canRemove: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), + canRestart: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), + + inputContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + inputWithFilesContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + + submitButtonDisabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), + submitButtonContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + + classNames: PropTypes.object.isRequired, + styles: PropTypes.object.isRequired, + addClassNames: PropTypes.object.isRequired, + + /* component injection */ + InputComponent: PropTypes.func, + PreviewComponent: PropTypes.func, + SubmitButtonComponent: PropTypes.func, + LayoutComponent: PropTypes.func, +} + +export default Dropzone +export { + LayoutDefault as Layout, + InputDefault as Input, + PreviewDefault as Preview, + SubmitButtonDefault as SubmitButton, + formatBytes, + formatDuration, + accepts, + defaultClassNames, + defaultGetFilesFromEvent as getFilesFromEvent, +} diff --git a/src/Input.js b/src/Input.js new file mode 100644 index 0000000..3ac34d5 --- /dev/null +++ b/src/Input.js @@ -0,0 +1,41 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +const Input = (props) => { + const { className, labelClassName, labelWithFilesClassName, style, labelStyle, labelWithFilesStyle, getFilesFromEvent, accept, multiple, disabled, content, withFilesContent, onFiles, files, } = props; + return (React.createElement("label", { className: files.length > 0 ? labelWithFilesClassName : labelClassName, style: files.length > 0 ? labelWithFilesStyle : labelStyle }, + files.length > 0 ? withFilesContent : content, + React.createElement("input", { className: className, style: style, type: "file", accept: accept, multiple: multiple, disabled: disabled, onChange: async (e) => { + const target = e.target; + const chosenFiles = await getFilesFromEvent(e); + onFiles(chosenFiles); + //@ts-ignore + target.value = null; + } }))); +}; +Input.propTypes = { + className: PropTypes.string, + labelClassName: PropTypes.string, + labelWithFilesClassName: PropTypes.string, + style: PropTypes.object, + labelStyle: PropTypes.object, + labelWithFilesStyle: PropTypes.object, + getFilesFromEvent: PropTypes.func.isRequired, + accept: PropTypes.string.isRequired, + multiple: PropTypes.bool.isRequired, + disabled: PropTypes.bool.isRequired, + content: PropTypes.node, + withFilesContent: PropTypes.node, + onFiles: PropTypes.func.isRequired, + files: PropTypes.arrayOf(PropTypes.any).isRequired, + extra: PropTypes.shape({ + active: PropTypes.bool.isRequired, + reject: PropTypes.bool.isRequired, + dragged: PropTypes.arrayOf(PropTypes.any).isRequired, + accept: PropTypes.string.isRequired, + multiple: PropTypes.bool.isRequired, + minSizeBytes: PropTypes.number.isRequired, + maxSizeBytes: PropTypes.number.isRequired, + maxFiles: PropTypes.number.isRequired, + }).isRequired, +}; +export default Input; diff --git a/src/Input.tsx b/src/Input.tsx index fb344b4..09e3f13 100644 --- a/src/Input.tsx +++ b/src/Input.tsx @@ -1,76 +1,76 @@ -import React from 'react' -import PropTypes from 'prop-types' - -import { IInputProps } from './Dropzone' - -const Input = (props: IInputProps) => { - const { - className, - labelClassName, - labelWithFilesClassName, - style, - labelStyle, - labelWithFilesStyle, - getFilesFromEvent, - accept, - multiple, - disabled, - content, - withFilesContent, - onFiles, - files, - } = props - - return ( - - ) -} - -Input.propTypes = { - className: PropTypes.string, - labelClassName: PropTypes.string, - labelWithFilesClassName: PropTypes.string, - style: PropTypes.object, - labelStyle: PropTypes.object, - labelWithFilesStyle: PropTypes.object, - getFilesFromEvent: PropTypes.func.isRequired, - accept: PropTypes.string.isRequired, - multiple: PropTypes.bool.isRequired, - disabled: PropTypes.bool.isRequired, - content: PropTypes.node, - withFilesContent: PropTypes.node, - onFiles: PropTypes.func.isRequired, - files: PropTypes.arrayOf(PropTypes.any).isRequired, - extra: PropTypes.shape({ - active: PropTypes.bool.isRequired, - reject: PropTypes.bool.isRequired, - dragged: PropTypes.arrayOf(PropTypes.any).isRequired, - accept: PropTypes.string.isRequired, - multiple: PropTypes.bool.isRequired, - minSizeBytes: PropTypes.number.isRequired, - maxSizeBytes: PropTypes.number.isRequired, - maxFiles: PropTypes.number.isRequired, - }).isRequired, -} - -export default Input +import React from 'react' +import PropTypes from 'prop-types' + +import { IInputProps } from './Dropzone' + +const Input = (props: IInputProps) => { + const { + className, + labelClassName, + labelWithFilesClassName, + style, + labelStyle, + labelWithFilesStyle, + getFilesFromEvent, + accept, + multiple, + disabled, + content, + withFilesContent, + onFiles, + files, + } = props + + return ( + + ) +} + +Input.propTypes = { + className: PropTypes.string, + labelClassName: PropTypes.string, + labelWithFilesClassName: PropTypes.string, + style: PropTypes.object, + labelStyle: PropTypes.object, + labelWithFilesStyle: PropTypes.object, + getFilesFromEvent: PropTypes.func.isRequired, + accept: PropTypes.string.isRequired, + multiple: PropTypes.bool.isRequired, + disabled: PropTypes.bool.isRequired, + content: PropTypes.node, + withFilesContent: PropTypes.node, + onFiles: PropTypes.func.isRequired, + files: PropTypes.arrayOf(PropTypes.any).isRequired, + extra: PropTypes.shape({ + active: PropTypes.bool.isRequired, + reject: PropTypes.bool.isRequired, + dragged: PropTypes.arrayOf(PropTypes.any).isRequired, + accept: PropTypes.string.isRequired, + multiple: PropTypes.bool.isRequired, + minSizeBytes: PropTypes.number.isRequired, + maxSizeBytes: PropTypes.number.isRequired, + maxFiles: PropTypes.number.isRequired, + }).isRequired, +} + +export default Input diff --git a/src/Layout.js b/src/Layout.js new file mode 100644 index 0000000..5eb7541 --- /dev/null +++ b/src/Layout.js @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +const Layout = (props) => { + const { input, previews, submitButton, dropzoneProps, files, extra: { maxFiles }, } = props; + return (React.createElement("div", Object.assign({}, dropzoneProps), + previews, + files.length < maxFiles && input, + files.length > 0 && submitButton)); +}; +Layout.propTypes = { + input: PropTypes.node, + previews: PropTypes.arrayOf(PropTypes.node), + submitButton: PropTypes.node, + dropzoneProps: PropTypes.shape({ + ref: PropTypes.any.isRequired, + className: PropTypes.string.isRequired, + style: PropTypes.object, + onDragEnter: PropTypes.func.isRequired, + onDragOver: PropTypes.func.isRequired, + onDragLeave: PropTypes.func.isRequired, + onDrop: PropTypes.func.isRequired, + }).isRequired, + files: PropTypes.arrayOf(PropTypes.any).isRequired, + extra: PropTypes.shape({ + active: PropTypes.bool.isRequired, + reject: PropTypes.bool.isRequired, + dragged: PropTypes.arrayOf(PropTypes.any).isRequired, + accept: PropTypes.string.isRequired, + multiple: PropTypes.bool.isRequired, + minSizeBytes: PropTypes.number.isRequired, + maxSizeBytes: PropTypes.number.isRequired, + maxFiles: PropTypes.number.isRequired, + onFiles: PropTypes.func.isRequired, + onCancelFile: PropTypes.func.isRequired, + onRemoveFile: PropTypes.func.isRequired, + onRestartFile: PropTypes.func.isRequired, + }).isRequired, +}; +export default Layout; diff --git a/src/Layout.tsx b/src/Layout.tsx index 9b8806d..a9e52f4 100644 --- a/src/Layout.tsx +++ b/src/Layout.tsx @@ -1,57 +1,57 @@ -import React from 'react' -import PropTypes from 'prop-types' - -import { ILayoutProps } from './Dropzone' - -const Layout = (props: ILayoutProps) => { - const { - input, - previews, - submitButton, - dropzoneProps, - files, - extra: { maxFiles }, - } = props - - return ( -
- {previews} - - {files.length < maxFiles && input} - - {files.length > 0 && submitButton} -
- ) -} - -Layout.propTypes = { - input: PropTypes.node, - previews: PropTypes.arrayOf(PropTypes.node), - submitButton: PropTypes.node, - dropzoneProps: PropTypes.shape({ - ref: PropTypes.any.isRequired, - className: PropTypes.string.isRequired, - style: PropTypes.object, - onDragEnter: PropTypes.func.isRequired, - onDragOver: PropTypes.func.isRequired, - onDragLeave: PropTypes.func.isRequired, - onDrop: PropTypes.func.isRequired, - }).isRequired, - files: PropTypes.arrayOf(PropTypes.any).isRequired, - extra: PropTypes.shape({ - active: PropTypes.bool.isRequired, - reject: PropTypes.bool.isRequired, - dragged: PropTypes.arrayOf(PropTypes.any).isRequired, - accept: PropTypes.string.isRequired, - multiple: PropTypes.bool.isRequired, - minSizeBytes: PropTypes.number.isRequired, - maxSizeBytes: PropTypes.number.isRequired, - maxFiles: PropTypes.number.isRequired, - onFiles: PropTypes.func.isRequired, - onCancelFile: PropTypes.func.isRequired, - onRemoveFile: PropTypes.func.isRequired, - onRestartFile: PropTypes.func.isRequired, - }).isRequired, -} - -export default Layout +import React from 'react' +import PropTypes from 'prop-types' + +import { ILayoutProps } from './Dropzone' + +const Layout = (props: ILayoutProps) => { + const { + input, + previews, + submitButton, + dropzoneProps, + files, + extra: { maxFiles }, + } = props + + return ( +
+ {previews} + + {files.length < maxFiles && input} + + {files.length > 0 && submitButton} +
+ ) +} + +Layout.propTypes = { + input: PropTypes.node, + previews: PropTypes.arrayOf(PropTypes.node), + submitButton: PropTypes.node, + dropzoneProps: PropTypes.shape({ + ref: PropTypes.any.isRequired, + className: PropTypes.string.isRequired, + style: PropTypes.object, + onDragEnter: PropTypes.func.isRequired, + onDragOver: PropTypes.func.isRequired, + onDragLeave: PropTypes.func.isRequired, + onDrop: PropTypes.func.isRequired, + }).isRequired, + files: PropTypes.arrayOf(PropTypes.any).isRequired, + extra: PropTypes.shape({ + active: PropTypes.bool.isRequired, + reject: PropTypes.bool.isRequired, + dragged: PropTypes.arrayOf(PropTypes.any).isRequired, + accept: PropTypes.string.isRequired, + multiple: PropTypes.bool.isRequired, + minSizeBytes: PropTypes.number.isRequired, + maxSizeBytes: PropTypes.number.isRequired, + maxFiles: PropTypes.number.isRequired, + onFiles: PropTypes.func.isRequired, + onCancelFile: PropTypes.func.isRequired, + onRemoveFile: PropTypes.func.isRequired, + onRestartFile: PropTypes.func.isRequired, + }).isRequired, +} + +export default Layout diff --git a/src/Preview.js b/src/Preview.js new file mode 100644 index 0000000..5979f59 --- /dev/null +++ b/src/Preview.js @@ -0,0 +1,104 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { formatBytes, formatDuration } from './utils'; +//@ts-ignore +import cancelImg from './assets/cancel.svg'; +//@ts-ignore +import removeImg from './assets/remove.svg'; +//@ts-ignore +import restartImg from './assets/restart.svg'; +const iconByFn = { + cancel: { backgroundImage: `url(${cancelImg})` }, + remove: { backgroundImage: `url(${removeImg})` }, + restart: { backgroundImage: `url(${restartImg})` }, +}; +class Preview extends React.PureComponent { + render() { + const { className, imageClassName, style, imageStyle, fileWithMeta: { cancel, remove, restart }, meta: { name = '', percent = 0, size = 0, previewUrl, status, duration, validationError }, isUpload, canCancel, canRemove, canRestart, extra: { minSizeBytes }, } = this.props; + let title = `${name || '?'}, ${formatBytes(size)}`; + if (duration) + title = `${title}, ${formatDuration(duration)}`; + if (status === 'error_file_size' || status === 'error_validation') { + return (React.createElement("div", { className: className, style: style }, + React.createElement("span", { className: "dzu-previewFileNameError" }, title), + status === 'error_file_size' && React.createElement("span", null, size < minSizeBytes ? 'File too small' : 'File too big'), + status === 'error_validation' && React.createElement("span", null, String(validationError)), + canRemove && React.createElement("span", { className: "dzu-previewButton", style: iconByFn.remove, onClick: remove }))); + } + if (status === 'error_upload_params' || status === 'exception_upload' || status === 'error_upload') { + title = `${title} (upload failed)`; + } + if (status === 'aborted') + title = `${title} (cancelled)`; + return (React.createElement("div", { className: className, style: style }, + previewUrl && React.createElement("img", { className: imageClassName, style: imageStyle, src: previewUrl, alt: title, title: title }), + !previewUrl && React.createElement("span", { className: "dzu-previewFileName" }, title), + React.createElement("div", { className: "dzu-previewStatusContainer" }, + isUpload && (React.createElement("progress", { max: 100, value: status === 'done' || status === 'headers_received' ? 100 : percent })), + status === 'uploading' && canCancel && (React.createElement("span", { className: "dzu-previewButton", style: iconByFn.cancel, onClick: cancel })), + status !== 'preparing' && status !== 'getting_upload_params' && status !== 'uploading' && canRemove && (React.createElement("span", { className: "dzu-previewButton", style: iconByFn.remove, onClick: remove })), + ['error_upload_params', 'exception_upload', 'error_upload', 'aborted', 'ready'].includes(status) && + canRestart && React.createElement("span", { className: "dzu-previewButton", style: iconByFn.restart, onClick: restart })))); + } +} +// @ts-ignore +Preview.propTypes = { + className: PropTypes.string, + imageClassName: PropTypes.string, + style: PropTypes.object, + imageStyle: PropTypes.object, + fileWithMeta: PropTypes.shape({ + file: PropTypes.any.isRequired, + meta: PropTypes.object.isRequired, + cancel: PropTypes.func.isRequired, + restart: PropTypes.func.isRequired, + remove: PropTypes.func.isRequired, + xhr: PropTypes.any, + }).isRequired, + // copy of fileWithMeta.meta, won't be mutated + meta: PropTypes.shape({ + status: PropTypes.oneOf([ + 'preparing', + 'error_file_size', + 'error_validation', + 'ready', + 'getting_upload_params', + 'error_upload_params', + 'uploading', + 'exception_upload', + 'aborted', + 'error_upload', + 'headers_received', + 'done', + ]).isRequired, + type: PropTypes.string.isRequired, + name: PropTypes.string, + uploadedDate: PropTypes.string.isRequired, + percent: PropTypes.number, + size: PropTypes.number, + lastModifiedDate: PropTypes.string, + previewUrl: PropTypes.string, + duration: PropTypes.number, + width: PropTypes.number, + height: PropTypes.number, + videoWidth: PropTypes.number, + videoHeight: PropTypes.number, + validationError: PropTypes.any, + }).isRequired, + isUpload: PropTypes.bool.isRequired, + canCancel: PropTypes.bool.isRequired, + canRemove: PropTypes.bool.isRequired, + canRestart: PropTypes.bool.isRequired, + files: PropTypes.arrayOf(PropTypes.any).isRequired, + extra: PropTypes.shape({ + active: PropTypes.bool.isRequired, + reject: PropTypes.bool.isRequired, + dragged: PropTypes.arrayOf(PropTypes.any).isRequired, + accept: PropTypes.string.isRequired, + multiple: PropTypes.bool.isRequired, + minSizeBytes: PropTypes.number.isRequired, + maxSizeBytes: PropTypes.number.isRequired, + maxFiles: PropTypes.number.isRequired, + }).isRequired, +}; +export default Preview; diff --git a/src/Preview.tsx b/src/Preview.tsx index 59122f5..ddc9772 100644 --- a/src/Preview.tsx +++ b/src/Preview.tsx @@ -1,139 +1,139 @@ -import React from 'react' -import PropTypes from 'prop-types' - -import { formatBytes, formatDuration } from './utils' -import { IPreviewProps } from './Dropzone' -//@ts-ignore -import cancelImg from './assets/cancel.svg' -//@ts-ignore -import removeImg from './assets/remove.svg' -//@ts-ignore -import restartImg from './assets/restart.svg' - -const iconByFn = { - cancel: { backgroundImage: `url(${cancelImg})` }, - remove: { backgroundImage: `url(${removeImg})` }, - restart: { backgroundImage: `url(${restartImg})` }, -} - -class Preview extends React.PureComponent { - render() { - const { - className, - imageClassName, - style, - imageStyle, - fileWithMeta: { cancel, remove, restart }, - meta: { name = '', percent = 0, size = 0, previewUrl, status, duration, validationError }, - isUpload, - canCancel, - canRemove, - canRestart, - extra: { minSizeBytes }, - } = this.props - - let title = `${name || '?'}, ${formatBytes(size)}` - if (duration) title = `${title}, ${formatDuration(duration)}` - - if (status === 'error_file_size' || status === 'error_validation') { - return ( -
- {title} - {status === 'error_file_size' && {size < minSizeBytes ? 'File too small' : 'File too big'}} - {status === 'error_validation' && {String(validationError)}} - {canRemove && } -
- ) - } - - if (status === 'error_upload_params' || status === 'exception_upload' || status === 'error_upload') { - title = `${title} (upload failed)` - } - if (status === 'aborted') title = `${title} (cancelled)` - - return ( -
- {previewUrl && {title}} - {!previewUrl && {title}} - -
- {isUpload && ( - - )} - - {status === 'uploading' && canCancel && ( - - )} - {status !== 'preparing' && status !== 'getting_upload_params' && status !== 'uploading' && canRemove && ( - - )} - {['error_upload_params', 'exception_upload', 'error_upload', 'aborted', 'ready'].includes(status) && - canRestart && } -
-
- ) - } -} - -// @ts-ignore -Preview.propTypes = { - className: PropTypes.string, - imageClassName: PropTypes.string, - style: PropTypes.object, - imageStyle: PropTypes.object, - fileWithMeta: PropTypes.shape({ - file: PropTypes.any.isRequired, - meta: PropTypes.object.isRequired, - cancel: PropTypes.func.isRequired, - restart: PropTypes.func.isRequired, - remove: PropTypes.func.isRequired, - xhr: PropTypes.any, - }).isRequired, - // copy of fileWithMeta.meta, won't be mutated - meta: PropTypes.shape({ - status: PropTypes.oneOf([ - 'preparing', - 'error_file_size', - 'error_validation', - 'ready', - 'getting_upload_params', - 'error_upload_params', - 'uploading', - 'exception_upload', - 'aborted', - 'error_upload', - 'headers_received', - 'done', - ]).isRequired, - type: PropTypes.string.isRequired, - name: PropTypes.string, - uploadedDate: PropTypes.string.isRequired, - percent: PropTypes.number, - size: PropTypes.number, - lastModifiedDate: PropTypes.string, - previewUrl: PropTypes.string, - duration: PropTypes.number, - width: PropTypes.number, - height: PropTypes.number, - videoWidth: PropTypes.number, - videoHeight: PropTypes.number, - validationError: PropTypes.any, - }).isRequired, - isUpload: PropTypes.bool.isRequired, - canCancel: PropTypes.bool.isRequired, - canRemove: PropTypes.bool.isRequired, - canRestart: PropTypes.bool.isRequired, - files: PropTypes.arrayOf(PropTypes.any).isRequired, // eslint-disable-line react/no-unused-prop-types - extra: PropTypes.shape({ - active: PropTypes.bool.isRequired, - reject: PropTypes.bool.isRequired, - dragged: PropTypes.arrayOf(PropTypes.any).isRequired, - accept: PropTypes.string.isRequired, - multiple: PropTypes.bool.isRequired, - minSizeBytes: PropTypes.number.isRequired, - maxSizeBytes: PropTypes.number.isRequired, - maxFiles: PropTypes.number.isRequired, - }).isRequired, -} - -export default Preview +import React from 'react' +import PropTypes from 'prop-types' + +import { formatBytes, formatDuration } from './utils' +import { IPreviewProps } from './Dropzone' +//@ts-ignore +import cancelImg from './assets/cancel.svg' +//@ts-ignore +import removeImg from './assets/remove.svg' +//@ts-ignore +import restartImg from './assets/restart.svg' + +const iconByFn = { + cancel: { backgroundImage: `url(${cancelImg})` }, + remove: { backgroundImage: `url(${removeImg})` }, + restart: { backgroundImage: `url(${restartImg})` }, +} + +class Preview extends React.PureComponent { + render() { + const { + className, + imageClassName, + style, + imageStyle, + fileWithMeta: { cancel, remove, restart }, + meta: { name = '', percent = 0, size = 0, previewUrl, status, duration, validationError }, + isUpload, + canCancel, + canRemove, + canRestart, + extra: { minSizeBytes }, + } = this.props + + let title = `${name || '?'}, ${formatBytes(size)}` + if (duration) title = `${title}, ${formatDuration(duration)}` + + if (status === 'error_file_size' || status === 'error_validation') { + return ( +
+ {title} + {status === 'error_file_size' && {size < minSizeBytes ? 'File too small' : 'File too big'}} + {status === 'error_validation' && {String(validationError)}} + {canRemove && } +
+ ) + } + + if (status === 'error_upload_params' || status === 'exception_upload' || status === 'error_upload') { + title = `${title} (upload failed)` + } + if (status === 'aborted') title = `${title} (cancelled)` + + return ( +
+ {previewUrl && {title}} + {!previewUrl && {title}} + +
+ {isUpload && ( + + )} + + {status === 'uploading' && canCancel && ( + + )} + {status !== 'preparing' && status !== 'getting_upload_params' && status !== 'uploading' && canRemove && ( + + )} + {['error_upload_params', 'exception_upload', 'error_upload', 'aborted', 'ready'].includes(status) && + canRestart && } +
+
+ ) + } +} + +// @ts-ignore +Preview.propTypes = { + className: PropTypes.string, + imageClassName: PropTypes.string, + style: PropTypes.object, + imageStyle: PropTypes.object, + fileWithMeta: PropTypes.shape({ + file: PropTypes.any.isRequired, + meta: PropTypes.object.isRequired, + cancel: PropTypes.func.isRequired, + restart: PropTypes.func.isRequired, + remove: PropTypes.func.isRequired, + xhr: PropTypes.any, + }).isRequired, + // copy of fileWithMeta.meta, won't be mutated + meta: PropTypes.shape({ + status: PropTypes.oneOf([ + 'preparing', + 'error_file_size', + 'error_validation', + 'ready', + 'getting_upload_params', + 'error_upload_params', + 'uploading', + 'exception_upload', + 'aborted', + 'error_upload', + 'headers_received', + 'done', + ]).isRequired, + type: PropTypes.string.isRequired, + name: PropTypes.string, + uploadedDate: PropTypes.string.isRequired, + percent: PropTypes.number, + size: PropTypes.number, + lastModifiedDate: PropTypes.string, + previewUrl: PropTypes.string, + duration: PropTypes.number, + width: PropTypes.number, + height: PropTypes.number, + videoWidth: PropTypes.number, + videoHeight: PropTypes.number, + validationError: PropTypes.any, + }).isRequired, + isUpload: PropTypes.bool.isRequired, + canCancel: PropTypes.bool.isRequired, + canRemove: PropTypes.bool.isRequired, + canRestart: PropTypes.bool.isRequired, + files: PropTypes.arrayOf(PropTypes.any).isRequired, // eslint-disable-line react/no-unused-prop-types + extra: PropTypes.shape({ + active: PropTypes.bool.isRequired, + reject: PropTypes.bool.isRequired, + dragged: PropTypes.arrayOf(PropTypes.any).isRequired, + accept: PropTypes.string.isRequired, + multiple: PropTypes.bool.isRequired, + minSizeBytes: PropTypes.number.isRequired, + maxSizeBytes: PropTypes.number.isRequired, + maxFiles: PropTypes.number.isRequired, + }).isRequired, +} + +export default Preview diff --git a/src/SubmitButton.js b/src/SubmitButton.js new file mode 100644 index 0000000..37e330a --- /dev/null +++ b/src/SubmitButton.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +const SubmitButton = (props) => { + const { className, buttonClassName, style, buttonStyle, disabled, content, onSubmit, files } = props; + const _disabled = files.some(f => ['preparing', 'getting_upload_params', 'uploading'].includes(f.meta.status)) || + !files.some(f => ['headers_received', 'done'].includes(f.meta.status)); + const handleSubmit = () => { + onSubmit(files.filter(f => ['headers_received', 'done'].includes(f.meta.status))); + }; + return (React.createElement("div", { className: className, style: style }, + React.createElement("button", { className: buttonClassName, style: buttonStyle, onClick: handleSubmit, disabled: disabled || _disabled }, content))); +}; +SubmitButton.propTypes = { + className: PropTypes.string, + buttonClassName: PropTypes.string, + style: PropTypes.object, + buttonStyle: PropTypes.object, + disabled: PropTypes.bool.isRequired, + content: PropTypes.node, + onSubmit: PropTypes.func.isRequired, + files: PropTypes.arrayOf(PropTypes.object).isRequired, + extra: PropTypes.shape({ + active: PropTypes.bool.isRequired, + reject: PropTypes.bool.isRequired, + dragged: PropTypes.arrayOf(PropTypes.any).isRequired, + accept: PropTypes.string.isRequired, + multiple: PropTypes.bool.isRequired, + minSizeBytes: PropTypes.number.isRequired, + maxSizeBytes: PropTypes.number.isRequired, + maxFiles: PropTypes.number.isRequired, + }).isRequired, +}; +export default SubmitButton; diff --git a/src/SubmitButton.tsx b/src/SubmitButton.tsx index b7706f5..8468714 100644 --- a/src/SubmitButton.tsx +++ b/src/SubmitButton.tsx @@ -1,47 +1,47 @@ -import React from 'react' -import PropTypes from 'prop-types' - -import { ISubmitButtonProps } from './Dropzone' - -const SubmitButton = (props: ISubmitButtonProps) => { - const { className, buttonClassName, style, buttonStyle, disabled, content, onSubmit, files } = props - - const _disabled = - files.some(f => ['preparing', 'getting_upload_params', 'uploading'].includes(f.meta.status)) || - !files.some(f => ['headers_received', 'done'].includes(f.meta.status)) - - const handleSubmit = () => { - onSubmit(files.filter(f => ['headers_received', 'done'].includes(f.meta.status))) - } - - return ( -
- -
- ) -} - -SubmitButton.propTypes = { - className: PropTypes.string, - buttonClassName: PropTypes.string, - style: PropTypes.object, - buttonStyle: PropTypes.object, - disabled: PropTypes.bool.isRequired, - content: PropTypes.node, - onSubmit: PropTypes.func.isRequired, - files: PropTypes.arrayOf(PropTypes.object).isRequired, - extra: PropTypes.shape({ - active: PropTypes.bool.isRequired, - reject: PropTypes.bool.isRequired, - dragged: PropTypes.arrayOf(PropTypes.any).isRequired, - accept: PropTypes.string.isRequired, - multiple: PropTypes.bool.isRequired, - minSizeBytes: PropTypes.number.isRequired, - maxSizeBytes: PropTypes.number.isRequired, - maxFiles: PropTypes.number.isRequired, - }).isRequired, -} - -export default SubmitButton +import React from 'react' +import PropTypes from 'prop-types' + +import { ISubmitButtonProps } from './Dropzone' + +const SubmitButton = (props: ISubmitButtonProps) => { + const { className, buttonClassName, style, buttonStyle, disabled, content, onSubmit, files } = props + + const _disabled = + files.some(f => ['preparing', 'getting_upload_params', 'uploading'].includes(f.meta.status)) || + !files.some(f => ['headers_received', 'done'].includes(f.meta.status)) + + const handleSubmit = () => { + onSubmit(files.filter(f => ['headers_received', 'done'].includes(f.meta.status))) + } + + return ( +
+ +
+ ) +} + +SubmitButton.propTypes = { + className: PropTypes.string, + buttonClassName: PropTypes.string, + style: PropTypes.object, + buttonStyle: PropTypes.object, + disabled: PropTypes.bool.isRequired, + content: PropTypes.node, + onSubmit: PropTypes.func.isRequired, + files: PropTypes.arrayOf(PropTypes.object).isRequired, + extra: PropTypes.shape({ + active: PropTypes.bool.isRequired, + reject: PropTypes.bool.isRequired, + dragged: PropTypes.arrayOf(PropTypes.any).isRequired, + accept: PropTypes.string.isRequired, + multiple: PropTypes.bool.isRequired, + minSizeBytes: PropTypes.number.isRequired, + maxSizeBytes: PropTypes.number.isRequired, + maxFiles: PropTypes.number.isRequired, + }).isRequired, +} + +export default SubmitButton diff --git a/src/assets/cancel.svg b/src/assets/cancel.svg index ed65005..5e89590 100644 --- a/src/assets/cancel.svg +++ b/src/assets/cancel.svg @@ -1 +1 @@ - + diff --git a/src/assets/remove.svg b/src/assets/remove.svg index f2b9074..74ae0ba 100644 --- a/src/assets/remove.svg +++ b/src/assets/remove.svg @@ -1 +1 @@ - + diff --git a/src/assets/restart.svg b/src/assets/restart.svg index 2349e7f..667ccde 100644 --- a/src/assets/restart.svg +++ b/src/assets/restart.svg @@ -1 +1 @@ - + diff --git a/src/styles.css b/src/styles.css index 8e04daa..6057398 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,140 +1,140 @@ -.dzu-dropzone { - display: flex; - flex-direction: column; - align-items: center; - width: 100%; - min-height: 120px; - overflow: scroll; - margin: 0 auto; - position: relative; - box-sizing: border-box; - transition: all .15s linear; - border: 2px solid #d9d9d9; - border-radius: 4px; -} - -.dzu-dropzoneActive { - background-color: #DEEBFF; - border-color: #2484FF; -} - -.dzu-dropzoneDisabled { - opacity: 0.5; -} - -.dzu-dropzoneDisabled *:hover { - cursor: unset; -} - -.dzu-input { - display: none; -} - -.dzu-inputLabel { - display: flex; - justify-content: center; - align-items: center; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - font-family: 'Helvetica', sans-serif; - font-size: 20px; - font-weight: 600; - color: #2484FF; - -moz-osx-font-smoothing: grayscale; - -webkit-font-smoothing: antialiased; - cursor: pointer; -} - -.dzu-inputLabelWithFiles { - display: flex; - justify-content: center; - align-items: center; - align-self: flex-start; - padding: 0 14px; - min-height: 32px; - background-color: #E6E6E6; - color: #2484FF; - border: none; - font-family: 'Helvetica', sans-serif; - border-radius: 4px; - font-size: 14px; - font-weight: 600; - margin-top: 20px; - margin-left: 3%; - -moz-osx-font-smoothing: grayscale; - -webkit-font-smoothing: antialiased; - cursor: pointer; -} - -.dzu-previewContainer { - padding: 40px 3%; - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - position: relative; - width: 100%; - min-height: 60px; - z-index: 1; - border-bottom: 1px solid #ECECEC; - box-sizing: border-box; -} - -.dzu-previewStatusContainer { - display: flex; - align-items: center; -} - -.dzu-previewFileName { - font-family: 'Helvetica', sans-serif; - font-size: 14px; - font-weight: 400; - color: #333333; -} - -.dzu-previewImage { - width: auto; - max-height: 40px; - max-width: 140px; - border-radius: 4px; -} - -.dzu-previewButton { - background-size: 14px 14px; - background-position: center; - background-repeat: no-repeat; - width: 14px; - height: 14px; - cursor: pointer; - opacity: 0.9; - margin: 0 0 2px 10px; -} - -.dzu-submitButtonContainer { - margin: 24px 0; - z-index: 1; -} - -.dzu-submitButton { - padding: 0 14px; - min-height: 32px; - background-color: #2484FF; - border: none; - border-radius: 4px; - font-family: 'Helvetica', sans-serif; - font-size: 14px; - font-weight: 600; - color: #FFF; - -moz-osx-font-smoothing: grayscale; - -webkit-font-smoothing: antialiased; - cursor: pointer; -} - -.dzu-submitButton:disabled { - background-color: #E6E6E6; - color: #333333; - cursor: unset; -} +.dzu-dropzone { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + min-height: 120px; + overflow: scroll; + margin: 0 auto; + position: relative; + box-sizing: border-box; + transition: all .15s linear; + border: 2px solid #d9d9d9; + border-radius: 4px; +} + +.dzu-dropzoneActive { + background-color: #DEEBFF; + border-color: #2484FF; +} + +.dzu-dropzoneDisabled { + opacity: 0.5; +} + +.dzu-dropzoneDisabled *:hover { + cursor: unset; +} + +.dzu-input { + display: none; +} + +.dzu-inputLabel { + display: flex; + justify-content: center; + align-items: center; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + font-family: 'Helvetica', sans-serif; + font-size: 20px; + font-weight: 600; + color: #2484FF; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + cursor: pointer; +} + +.dzu-inputLabelWithFiles { + display: flex; + justify-content: center; + align-items: center; + align-self: flex-start; + padding: 0 14px; + min-height: 32px; + background-color: #E6E6E6; + color: #2484FF; + border: none; + font-family: 'Helvetica', sans-serif; + border-radius: 4px; + font-size: 14px; + font-weight: 600; + margin-top: 20px; + margin-left: 3%; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + cursor: pointer; +} + +.dzu-previewContainer { + padding: 40px 3%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + position: relative; + width: 100%; + min-height: 60px; + z-index: 1; + border-bottom: 1px solid #ECECEC; + box-sizing: border-box; +} + +.dzu-previewStatusContainer { + display: flex; + align-items: center; +} + +.dzu-previewFileName { + font-family: 'Helvetica', sans-serif; + font-size: 14px; + font-weight: 400; + color: #333333; +} + +.dzu-previewImage { + width: auto; + max-height: 40px; + max-width: 140px; + border-radius: 4px; +} + +.dzu-previewButton { + background-size: 14px 14px; + background-position: center; + background-repeat: no-repeat; + width: 14px; + height: 14px; + cursor: pointer; + opacity: 0.9; + margin: 0 0 2px 10px; +} + +.dzu-submitButtonContainer { + margin: 24px 0; + z-index: 1; +} + +.dzu-submitButton { + padding: 0 14px; + min-height: 32px; + background-color: #2484FF; + border: none; + border-radius: 4px; + font-family: 'Helvetica', sans-serif; + font-size: 14px; + font-weight: 600; + color: #FFF; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + cursor: pointer; +} + +.dzu-submitButton:disabled { + background-color: #E6E6E6; + color: #333333; + cursor: unset; +} diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..cb65107 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,90 @@ +export const formatBytes = (b) => { + const units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + let l = 0; + let n = b; + while (n >= 1024) { + n /= 1024; + l += 1; + } + return `${n.toFixed(n >= 10 || l < 1 ? 0 : 1)}${units[l]}`; +}; +export const formatDuration = (seconds) => { + const date = new Date(0); + date.setSeconds(seconds); + const dateString = date.toISOString().slice(11, 19); + if (seconds < 3600) + return dateString.slice(3); + return dateString; +}; +// adapted from: https://github.com/okonet/attr-accept/blob/master/src/index.js +// returns true if file.name is empty and accept string is something like ".csv", +// because file comes from dataTransferItem for drag events, and +// dataTransferItem.name is always empty +export const accepts = (file, accept) => { + if (!accept || accept === '*') + return true; + const mimeType = file.type || ''; + const baseMimeType = mimeType.replace(/\/.*$/, ''); + return accept + .split(',') + .map(t => t.trim()) + .some(type => { + if (type.charAt(0) === '.') { + return file.name === undefined || file.name.toLowerCase().endsWith(type.toLowerCase()); + } + else if (type.endsWith('/*')) { + // this is something like an image/* mime type + return baseMimeType === type.replace(/\/.*$/, ''); + } + return mimeType === type; + }); +}; +export const resolveValue = (value, ...args) => { + if (typeof value === 'function') + return value(...args); + return value; +}; +export const defaultClassNames = { + dropzone: 'dzu-dropzone', + dropzoneActive: 'dzu-dropzoneActive', + dropzoneReject: 'dzu-dropzoneActive', + dropzoneDisabled: 'dzu-dropzoneDisabled', + input: 'dzu-input', + inputLabel: 'dzu-inputLabel', + inputLabelWithFiles: 'dzu-inputLabelWithFiles', + preview: 'dzu-previewContainer', + previewImage: 'dzu-previewImage', + submitButtonContainer: 'dzu-submitButtonContainer', + submitButton: 'dzu-submitButton', +}; +export const mergeStyles = (classNames, styles, addClassNames, ...args) => { + const resolvedClassNames = { ...defaultClassNames }; + const resolvedStyles = { ...styles }; + for (const [key, value] of Object.entries(classNames)) { + resolvedClassNames[key] = resolveValue(value, ...args); + } + for (const [key, value] of Object.entries(addClassNames)) { + resolvedClassNames[key] = `${resolvedClassNames[key]} ${resolveValue(value, ...args)}`; + } + for (const [key, value] of Object.entries(styles)) { + resolvedStyles[key] = resolveValue(value, ...args); + } + return { classNames: resolvedClassNames, styles: resolvedStyles }; +}; +export const getFilesFromEvent = (event) => { + let items = null; + if ('dataTransfer' in event) { + const dt = event.dataTransfer; + // NOTE: Only the 'drop' event has access to DataTransfer.files, otherwise it will always be empty + if ('files' in dt && dt.files.length) { + items = dt.files; + } + else if (dt.items && dt.items.length) { + items = dt.items; + } + } + else if (event.target && event.target.files) { + items = event.target.files; + } + return Array.prototype.slice.call(items); +}; diff --git a/src/utils.ts b/src/utils.ts index 707918d..2701d6f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,113 +1,113 @@ -import React from 'react' -import { IStyleCustomization } from './Dropzone' - -export const formatBytes = (b: number) => { - const units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] - let l = 0 - let n = b - - while (n >= 1024) { - n /= 1024 - l += 1 - } - - return `${n.toFixed(n >= 10 || l < 1 ? 0 : 1)}${units[l]}` -} - -export const formatDuration = (seconds: number) => { - const date = new Date(0) - date.setSeconds(seconds) - const dateString = date.toISOString().slice(11, 19) - if (seconds < 3600) return dateString.slice(3) - return dateString -} - -// adapted from: https://github.com/okonet/attr-accept/blob/master/src/index.js -// returns true if file.name is empty and accept string is something like ".csv", -// because file comes from dataTransferItem for drag events, and -// dataTransferItem.name is always empty -export const accepts = (file: File, accept: string) => { - if (!accept || accept === '*') return true - - const mimeType = file.type || '' - const baseMimeType = mimeType.replace(/\/.*$/, '') - - return accept - .split(',') - .map(t => t.trim()) - .some(type => { - if (type.charAt(0) === '.') { - return file.name === undefined || file.name.toLowerCase().endsWith(type.toLowerCase()) - } else if (type.endsWith('/*')) { - // this is something like an image/* mime type - return baseMimeType === type.replace(/\/.*$/, '') - } - return mimeType === type - }) -} - -type ResolveFn = (...args: any[]) => T - -export const resolveValue = (value: ResolveFn | T, ...args: any[]) => { - if (typeof value === 'function') return (value as ResolveFn)(...args) - return value -} - -export const defaultClassNames = { - dropzone: 'dzu-dropzone', - dropzoneActive: 'dzu-dropzoneActive', - dropzoneReject: 'dzu-dropzoneActive', - dropzoneDisabled: 'dzu-dropzoneDisabled', - input: 'dzu-input', - inputLabel: 'dzu-inputLabel', - inputLabelWithFiles: 'dzu-inputLabelWithFiles', - preview: 'dzu-previewContainer', - previewImage: 'dzu-previewImage', - submitButtonContainer: 'dzu-submitButtonContainer', - submitButton: 'dzu-submitButton', -} - -export const mergeStyles = ( - classNames: IStyleCustomization, - styles: IStyleCustomization, - addClassNames: IStyleCustomization, - ...args: any[] -) => { - const resolvedClassNames: { [property: string]: string } = { ...defaultClassNames } - const resolvedStyles = { ...styles } as { [property: string]: string } - - for (const [key, value] of Object.entries(classNames)) { - resolvedClassNames[key] = resolveValue(value, ...args) - } - - for (const [key, value] of Object.entries(addClassNames)) { - resolvedClassNames[key] = `${resolvedClassNames[key]} ${resolveValue(value, ...args)}` - } - - for (const [key, value] of Object.entries(styles)) { - resolvedStyles[key] = resolveValue(value, ...args) - } - - return { classNames: resolvedClassNames, styles: resolvedStyles as IStyleCustomization } -} - -export const getFilesFromEvent = ( - event: React.DragEvent | React.ChangeEvent, -): Array => { - let items = null - - if ('dataTransfer' in event) { - const dt = event.dataTransfer - - // NOTE: Only the 'drop' event has access to DataTransfer.files, otherwise it will always be empty - if ('files' in dt && dt.files.length) { - items = dt.files - } else if (dt.items && dt.items.length) { - items = dt.items - } - } else if (event.target && event.target.files) { - items = event.target.files - } - - return Array.prototype.slice.call(items) -} +import React from 'react' +import { IStyleCustomization } from './Dropzone' + +export const formatBytes = (b: number) => { + const units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + let l = 0 + let n = b + + while (n >= 1024) { + n /= 1024 + l += 1 + } + + return `${n.toFixed(n >= 10 || l < 1 ? 0 : 1)}${units[l]}` +} + +export const formatDuration = (seconds: number) => { + const date = new Date(0) + date.setSeconds(seconds) + const dateString = date.toISOString().slice(11, 19) + if (seconds < 3600) return dateString.slice(3) + return dateString +} + +// adapted from: https://github.com/okonet/attr-accept/blob/master/src/index.js +// returns true if file.name is empty and accept string is something like ".csv", +// because file comes from dataTransferItem for drag events, and +// dataTransferItem.name is always empty +export const accepts = (file: File, accept: string) => { + if (!accept || accept === '*') return true + + const mimeType = file.type || '' + const baseMimeType = mimeType.replace(/\/.*$/, '') + + return accept + .split(',') + .map(t => t.trim()) + .some(type => { + if (type.charAt(0) === '.') { + return file.name === undefined || file.name.toLowerCase().endsWith(type.toLowerCase()) + } else if (type.endsWith('/*')) { + // this is something like an image/* mime type + return baseMimeType === type.replace(/\/.*$/, '') + } + return mimeType === type + }) +} + +type ResolveFn = (...args: any[]) => T + +export const resolveValue = (value: ResolveFn | T, ...args: any[]) => { + if (typeof value === 'function') return (value as ResolveFn)(...args) + return value +} + +export const defaultClassNames = { + dropzone: 'dzu-dropzone', + dropzoneActive: 'dzu-dropzoneActive', + dropzoneReject: 'dzu-dropzoneActive', + dropzoneDisabled: 'dzu-dropzoneDisabled', + input: 'dzu-input', + inputLabel: 'dzu-inputLabel', + inputLabelWithFiles: 'dzu-inputLabelWithFiles', + preview: 'dzu-previewContainer', + previewImage: 'dzu-previewImage', + submitButtonContainer: 'dzu-submitButtonContainer', + submitButton: 'dzu-submitButton', +} + +export const mergeStyles = ( + classNames: IStyleCustomization, + styles: IStyleCustomization, + addClassNames: IStyleCustomization, + ...args: any[] +) => { + const resolvedClassNames: { [property: string]: string } = { ...defaultClassNames } + const resolvedStyles = { ...styles } as { [property: string]: string } + + for (const [key, value] of Object.entries(classNames)) { + resolvedClassNames[key] = resolveValue(value, ...args) + } + + for (const [key, value] of Object.entries(addClassNames)) { + resolvedClassNames[key] = `${resolvedClassNames[key]} ${resolveValue(value, ...args)}` + } + + for (const [key, value] of Object.entries(styles)) { + resolvedStyles[key] = resolveValue(value, ...args) + } + + return { classNames: resolvedClassNames, styles: resolvedStyles as IStyleCustomization } +} + +export const getFilesFromEvent = ( + event: React.DragEvent | React.ChangeEvent, +): Array => { + let items = null + + if ('dataTransfer' in event) { + const dt = event.dataTransfer + + // NOTE: Only the 'drop' event has access to DataTransfer.files, otherwise it will always be empty + if ('files' in dt && dt.files.length) { + items = dt.files + } else if (dt.items && dt.items.length) { + items = dt.items + } + } else if (event.target && event.target.files) { + items = event.target.files + } + + return Array.prototype.slice.call(items) +} diff --git a/styleguide-quickstart.config.js b/styleguide-quickstart.config.js index 04d8f46..513630a 100644 --- a/styleguide-quickstart.config.js +++ b/styleguide-quickstart.config.js @@ -1,38 +1,38 @@ -/* eslint import/no-extraneous-dependencies: 0 */ -/* eslint global-require: 0 */ -const path = require('path') - -module.exports = { - title: 'Quickstart · React Dropzone Uploader', - styleguideDir: path.join(__dirname, 'docs/assets/styleguide-quickstart'), - webpackConfig: require('./webpack.config.js'), - require: [ - path.join(__dirname, 'src', 'styles.css'), - path.resolve(__dirname, 'styleguide.setup.js'), - ], - exampleMode: 'expand', - usageMode: 'expand', - showSidebar: false, - serverPort: 8080, - compilerConfig: { - transforms: { dangerousTaggedTemplateString: true }, - objectAssign: 'Object.assign', - }, - styles: { - StyleGuide: { - content: { - padding: [[16, 0]], - }, - }, - Heading: { - heading1: { - fontSize: 32, - }, - }, - }, - sections: [ - { - content: 'examples/Quickstart.md', - }, - ], -} +/* eslint import/no-extraneous-dependencies: 0 */ +/* eslint global-require: 0 */ +const path = require('path') + +module.exports = { + title: 'Quickstart · React Dropzone Uploader', + styleguideDir: path.join(__dirname, 'docs/assets/styleguide-quickstart'), + webpackConfig: require('./webpack.config.js'), + require: [ + path.join(__dirname, 'src', 'styles.css'), + path.resolve(__dirname, 'styleguide.setup.js'), + ], + exampleMode: 'expand', + usageMode: 'expand', + showSidebar: false, + serverPort: 8080, + compilerConfig: { + transforms: { dangerousTaggedTemplateString: true }, + objectAssign: 'Object.assign', + }, + styles: { + StyleGuide: { + content: { + padding: [[16, 0]], + }, + }, + Heading: { + heading1: { + fontSize: 32, + }, + }, + }, + sections: [ + { + content: 'examples/Quickstart.md', + }, + ], +} diff --git a/styleguide.config.js b/styleguide.config.js index efdbc65..df94d17 100644 --- a/styleguide.config.js +++ b/styleguide.config.js @@ -1,76 +1,76 @@ -/* eslint import/no-extraneous-dependencies: 0 */ -/* eslint global-require: 0 */ -const path = require('path') - -module.exports = { - title: 'Live Examples · React Dropzone Uploader', - styleguideDir: path.join(__dirname, 'docs/assets/styleguide'), - webpackConfig: require('./webpack.config.js'), - require: [ - path.join(__dirname, 'src', 'styles.css'), - path.join(__dirname, 'examples', 'styles.css'), - path.resolve(__dirname, 'styleguide.setup.js'), - ], - exampleMode: 'expand', - usageMode: 'expand', - showSidebar: false, - serverPort: 8080, - compilerConfig: { - transforms: { dangerousTaggedTemplateString: true }, - objectAssign: 'Object.assign', - }, - styles: { - StyleGuide: { - content: { - padding: [[16, 0]], - }, - }, - Heading: { - heading1: { - fontSize: 32, - }, - }, - }, - sections: [ - { - name: 'Standard', - content: 'examples/Standard.md', - }, - { - name: 'Only Image, Audio, Video', - content: 'examples/Accept.md', - }, - { - name: 'No Upload', - content: 'examples/NoUpload.md', - }, - { - name: 'Single File, Auto Submit', - content: 'examples/SingleFile.md', - }, - { - name: 'Custom Preview', - content: 'examples/CustomPreview.md', - }, - { - name: 'Custom Layout', - content: 'examples/CustomLayout.md', - }, - { - name: 'Custom Input, Directory Drag and Drop', - content: 'examples/CustomInput.md', - }, - { - name: 'Dropzone With No Input', - content: 'examples/NoInput.md', - }, - { - name: 'Input With No Dropzone', - content: 'examples/NoDropzone.md', - }, - { - name: 'Initial File From Data URL', - content: 'examples/InitialFileFromDataUrl.md', - }, - ], -} +/* eslint import/no-extraneous-dependencies: 0 */ +/* eslint global-require: 0 */ +const path = require('path') + +module.exports = { + title: 'Live Examples · React Dropzone Uploader', + styleguideDir: path.join(__dirname, 'docs/assets/styleguide'), + webpackConfig: require('./webpack.config.js'), + require: [ + path.join(__dirname, 'src', 'styles.css'), + path.join(__dirname, 'examples', 'styles.css'), + path.resolve(__dirname, 'styleguide.setup.js'), + ], + exampleMode: 'expand', + usageMode: 'expand', + showSidebar: false, + serverPort: 8080, + compilerConfig: { + transforms: { dangerousTaggedTemplateString: true }, + objectAssign: 'Object.assign', + }, + styles: { + StyleGuide: { + content: { + padding: [[16, 0]], + }, + }, + Heading: { + heading1: { + fontSize: 32, + }, + }, + }, + sections: [ + { + name: 'Standard', + content: 'examples/Standard.md', + }, + { + name: 'Only Image, Audio, Video', + content: 'examples/Accept.md', + }, + { + name: 'No Upload', + content: 'examples/NoUpload.md', + }, + { + name: 'Single File, Auto Submit', + content: 'examples/SingleFile.md', + }, + { + name: 'Custom Preview', + content: 'examples/CustomPreview.md', + }, + { + name: 'Custom Layout', + content: 'examples/CustomLayout.md', + }, + { + name: 'Custom Input, Directory Drag and Drop', + content: 'examples/CustomInput.md', + }, + { + name: 'Dropzone With No Input', + content: 'examples/NoInput.md', + }, + { + name: 'Input With No Dropzone', + content: 'examples/NoDropzone.md', + }, + { + name: 'Initial File From Data URL', + content: 'examples/InitialFileFromDataUrl.md', + }, + ], +} diff --git a/styleguide.setup.js b/styleguide.setup.js index ac89948..4be5754 100644 --- a/styleguide.setup.js +++ b/styleguide.setup.js @@ -1,8 +1,8 @@ -import { getDroppedOrSelectedFiles, getDataTransferFiles } from 'html5-file-selector' - -import Dropzone, { defaultClassNames } from './src/Dropzone' - -global.getDroppedOrSelectedFiles = getDroppedOrSelectedFiles -global.getDataTransferFiles = getDataTransferFiles -global.Dropzone = Dropzone -global.defaultClassNames = defaultClassNames +import { getDroppedOrSelectedFiles, getDataTransferFiles } from 'html5-file-selector' + +import Dropzone, { defaultClassNames } from './src/Dropzone' + +global.getDroppedOrSelectedFiles = getDroppedOrSelectedFiles +global.getDataTransferFiles = getDataTransferFiles +global.Dropzone = Dropzone +global.defaultClassNames = defaultClassNames diff --git a/tests/Dropzone.test.js b/tests/Dropzone.test.js index 67773aa..2f5a5fd 100644 --- a/tests/Dropzone.test.js +++ b/tests/Dropzone.test.js @@ -1,34 +1,34 @@ -import Dropzone, { Input, Preview, SubmitButton } from '../src/Dropzone' -import { formatBytes, formatDuration, accepts, defaultClassNames, mergeStyles } from '../src/utils' - -test('formatBytes', () => { - expect(formatBytes(2000)).toEqual('2.0kB') -}) - -test('formatBytes', () => { - expect(formatBytes(1024 * 1024 * 1.5)).toEqual('1.5MB') -}) - -test('formatDuration', () => { - expect(formatDuration(1000)).toEqual('16:40') -}) - -test('accepts', () => { - const file = { name: 'image.png', type: 'image/png' } - expect(accepts(file, '*')).toEqual(true) - expect(accepts(file, '.png,.jpg')).toEqual(true) - expect(accepts(file, 'image/*,video/*')).toEqual(true) - expect(accepts(file, 'audio/*')).toEqual(false) -}) - -test('mergeStyles', () => { - const _classNames = { dropzone: 'dz', inputLabel: v => v } - const _styles = { dropzone: { color: 'red' } } - const _addClassNames = { input: 'dz' } - const { classNames, styles } = mergeStyles(_classNames, _styles, _addClassNames, 'name') - - expect(classNames.dropzone).toEqual('dz') - expect(styles.dropzone).toEqual({ color: 'red' }) - expect(classNames.input).toEqual(`${defaultClassNames.input} dz`) - expect(classNames.inputLabel).toEqual('name') -}) +import Dropzone, { Input, Preview, SubmitButton } from '../src/Dropzone' +import { formatBytes, formatDuration, accepts, defaultClassNames, mergeStyles } from '../src/utils' + +test('formatBytes', () => { + expect(formatBytes(2000)).toEqual('2.0kB') +}) + +test('formatBytes', () => { + expect(formatBytes(1024 * 1024 * 1.5)).toEqual('1.5MB') +}) + +test('formatDuration', () => { + expect(formatDuration(1000)).toEqual('16:40') +}) + +test('accepts', () => { + const file = { name: 'image.png', type: 'image/png' } + expect(accepts(file, '*')).toEqual(true) + expect(accepts(file, '.png,.jpg')).toEqual(true) + expect(accepts(file, 'image/*,video/*')).toEqual(true) + expect(accepts(file, 'audio/*')).toEqual(false) +}) + +test('mergeStyles', () => { + const _classNames = { dropzone: 'dz', inputLabel: v => v } + const _styles = { dropzone: { color: 'red' } } + const _addClassNames = { input: 'dz' } + const { classNames, styles } = mergeStyles(_classNames, _styles, _addClassNames, 'name') + + expect(classNames.dropzone).toEqual('dz') + expect(styles.dropzone).toEqual({ color: 'red' }) + expect(classNames.input).toEqual(`${defaultClassNames.input} dz`) + expect(classNames.inputLabel).toEqual('name') +}) diff --git a/tests/fileMock.js b/tests/fileMock.js index 0e56c5b..7a373fa 100644 --- a/tests/fileMock.js +++ b/tests/fileMock.js @@ -1 +1 @@ -module.exports = 'test-file-stub' +module.exports = 'test-file-stub' diff --git a/tests/test-umd.html b/tests/test-umd.html index 0fe55b6..6938c5f 100644 --- a/tests/test-umd.html +++ b/tests/test-umd.html @@ -1,55 +1,55 @@ - - - - - - - - - - - - - - Document - - -
- - - - - + + + + + + + + + + + + + + Document + + +
+ + + + + diff --git a/tsconfig.dev.json b/tsconfig.dev.json index 4611e46..6b8344c 100644 --- a/tsconfig.dev.json +++ b/tsconfig.dev.json @@ -1,13 +1,13 @@ -{ -"compilerOptions": { - "target": "ESNext", - "esModuleInterop": true, - "jsx": "react", - "skipLibCheck": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "strict": true - }, - "include": ["src/*.tsx", "src/*.ts", "examples/src/*.tsx", "examples/src/*.ts"], - "exclude": ["node_modules"] -} +{ +"compilerOptions": { + "target": "ESNext", + "esModuleInterop": true, + "jsx": "react", + "skipLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "strict": true + }, + "include": ["src/*.tsx", "src/*.ts", "examples/src/*.tsx", "examples/src/*.ts"], + "exclude": ["node_modules"] +} diff --git a/tsconfig.json b/tsconfig.json index 193b451..d869ce4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,13 @@ -{ - "compilerOptions": { - "target": "ESNext", - "esModuleInterop": true, - "jsx": "react", - "skipLibCheck": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "strict": true - }, - "include": ["src/*.tsx", "src/*.ts"], - "exclude": ["node_modules"] -} +{ + "compilerOptions": { + "target": "ESNext", + "esModuleInterop": true, + "jsx": "react", + "skipLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "strict": true + }, + "include": ["src/*.tsx", "src/*.ts"], + "exclude": ["node_modules"] +} diff --git a/update_styleguides.py b/update_styleguides.py index 0a2b290..a22ed72 100755 --- a/update_styleguides.py +++ b/update_styleguides.py @@ -1,34 +1,34 @@ -import os - - -print('\nupdate styleguides') - -with open('docs/quick-start.md', 'r') as infile: - lines = infile.readlines() -with open('docs/quick-start.md', 'w') as f: - for line in lines: - if not line.startswith('\n') - path = f'docs/assets/styleguide-quickstart/build/{file}' - with open(path, 'r') as script: - text = script.read().replace('"build/"', '"assets/styleguide-quickstart/build/"') - with open(path, 'w') as script: - script.write(text) - -with open('docs/examples.md', 'r') as infile: - lines = infile.readlines() -with open('docs/examples.md', 'w') as f: - for line in lines: - if not line.startswith('\n') - path = f'docs/assets/styleguide/build/{file}' - with open(path, 'r') as script: - text = script.read().replace('"build/"', '"assets/styleguide/build/"') - with open(path, 'w') as script: - script.write(text) +import os + + +print('\nupdate styleguides') + +with open('docs/quick-start.md', 'r') as infile: + lines = infile.readlines() +with open('docs/quick-start.md', 'w') as f: + for line in lines: + if not line.startswith('\n') + path = f'docs/assets/styleguide-quickstart/build/{file}' + with open(path, 'r') as script: + text = script.read().replace('"build/"', '"assets/styleguide-quickstart/build/"') + with open(path, 'w') as script: + script.write(text) + +with open('docs/examples.md', 'r') as infile: + lines = infile.readlines() +with open('docs/examples.md', 'w') as f: + for line in lines: + if not line.startswith('\n') + path = f'docs/assets/styleguide/build/{file}' + with open(path, 'r') as script: + text = script.read().replace('"build/"', '"assets/styleguide/build/"') + with open(path, 'w') as script: + script.write(text) diff --git a/webpack.build.config.js b/webpack.build.config.js index 6636b27..eed9280 100644 --- a/webpack.build.config.js +++ b/webpack.build.config.js @@ -1,50 +1,50 @@ -/* eslint import/no-extraneous-dependencies: 0 */ -const UglifyJSPlugin = require('uglifyjs-webpack-plugin') -const path = require('path') - -if (process.env.NODE_ENV !== 'production') { - throw new Error('Production builds must have NODE_ENV=production.') -} - -function createConfig(entry, output) { - return { - mode: 'production', - entry, - output, - optimization: { - minimizer: [new UglifyJSPlugin()], - }, - module: { - rules: [ - { - test: /\.js?$/, - exclude: /node_modules/, - use: 'babel-loader', - }, - { - test: /\.css$/, - loaders: ['style-loader', 'css-loader'], - }, - { - test: /\.(png|jpg|jpeg|gif|svg|woff|woff2)$/, - exclude: /node_modules/, - loader: 'url-loader?limit=10000', - }, - ], - }, - } -} - -module.exports = [ - createConfig('./src/Dropzone.js', { - path: path.resolve('dist'), - libraryTarget: 'commonjs2', - filename: 'react-dropzone-uploader.js', - }), - createConfig('./src/Dropzone.js', { - path: path.resolve('dist'), - libraryTarget: 'umd', - filename: 'react-dropzone-uploader.umd.js', - library: 'ReactDropzoneUploader', - }), -] +/* eslint import/no-extraneous-dependencies: 0 */ +const UglifyJSPlugin = require('uglifyjs-webpack-plugin') +const path = require('path') + +if (process.env.NODE_ENV !== 'production') { + throw new Error('Production builds must have NODE_ENV=production.') +} + +function createConfig(entry, output) { + return { + mode: 'production', + entry, + output, + optimization: { + minimizer: [new UglifyJSPlugin()], + }, + module: { + rules: [ + { + test: /\.js?$/, + exclude: /node_modules/, + use: 'babel-loader', + }, + { + test: /\.css$/, + loaders: ['style-loader', 'css-loader'], + }, + { + test: /\.(png|jpg|jpeg|gif|svg|woff|woff2)$/, + exclude: /node_modules/, + loader: 'url-loader?limit=10000', + }, + ], + }, + } +} + +module.exports = [ + createConfig('./src/Dropzone.js', { + path: path.resolve('dist'), + libraryTarget: 'commonjs2', + filename: 'react-dropzone-uploader.js', + }), + createConfig('./src/Dropzone.js', { + path: path.resolve('dist'), + libraryTarget: 'umd', + filename: 'react-dropzone-uploader.umd.js', + library: 'ReactDropzoneUploader', + }), +] diff --git a/webpack.config.js b/webpack.config.js index 85ece69..dba4382 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,32 +1,32 @@ -const path = require('path') - -module.exports = { - devtool: 'inline-source-map', - entry: './examples/src/index.js', - output: { - filename: 'bundle.js', - path: path.resolve(__dirname, 'examples', 'dist'), - publicPath: '/', - }, - module: { - rules: [ - { - test: /\.js?$/, - exclude: /node_modules/, - use: 'babel-loader', - }, - { - test: /\.css$/, - loaders: ['style-loader', 'css-loader'], - }, - { - test: /\.(png|jpg|jpeg|gif|svg|woff|woff2)$/, - exclude: /node_modules/, - loader: 'url-loader?limit=10000', - }, - ], - }, - devServer: { - contentBase: path.resolve(__dirname, 'examples', 'dist'), - }, -} +const path = require('path') + +module.exports = { + devtool: 'inline-source-map', + entry: './examples/src/index.js', + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, 'examples', 'dist'), + publicPath: '/', + }, + module: { + rules: [ + { + test: /\.js?$/, + exclude: /node_modules/, + use: 'babel-loader', + }, + { + test: /\.css$/, + loaders: ['style-loader', 'css-loader'], + }, + { + test: /\.(png|jpg|jpeg|gif|svg|woff|woff2)$/, + exclude: /node_modules/, + loader: 'url-loader?limit=10000', + }, + ], + }, + devServer: { + contentBase: path.resolve(__dirname, 'examples', 'dist'), + }, +} diff --git a/website/core/Footer.js b/website/core/Footer.js index 702213a..7dd7cca 100644 --- a/website/core/Footer.js +++ b/website/core/Footer.js @@ -1,119 +1,119 @@ -/** - * Copyright (c) 2017-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -const React = require('react'); - -class Footer extends React.Component { - docUrl(doc, language) { - const baseUrl = this.props.config.baseUrl; - return `${baseUrl}docs/${language ? `${language}/` : ''}${doc}`; - } - - pageUrl(doc, language) { - const baseUrl = this.props.config.baseUrl; - return baseUrl + (language ? `${language}/` : '') + doc; - } - - render() { - return ( - - ); - } -} - -module.exports = Footer; +/** + * Copyright (c) 2017-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const React = require('react'); + +class Footer extends React.Component { + docUrl(doc, language) { + const baseUrl = this.props.config.baseUrl; + return `${baseUrl}docs/${language ? `${language}/` : ''}${doc}`; + } + + pageUrl(doc, language) { + const baseUrl = this.props.config.baseUrl; + return baseUrl + (language ? `${language}/` : '') + doc; + } + + render() { + return ( + + ); + } +} + +module.exports = Footer; diff --git a/website/package.json b/website/package.json index 04776b1..f0cd937 100644 --- a/website/package.json +++ b/website/package.json @@ -1,14 +1,14 @@ -{ - "scripts": { - "examples": "docusaurus-examples", - "start": "docusaurus-start", - "build": "docusaurus-build", - "publish-gh-pages": "docusaurus-publish", - "write-translations": "docusaurus-write-translations", - "version": "docusaurus-version", - "rename-version": "docusaurus-rename-version" - }, - "devDependencies": { - "docusaurus": "^1.5.1" - } -} +{ + "scripts": { + "examples": "docusaurus-examples", + "start": "docusaurus-start", + "build": "docusaurus-build", + "publish-gh-pages": "docusaurus-publish", + "write-translations": "docusaurus-write-translations", + "version": "docusaurus-version", + "rename-version": "docusaurus-rename-version" + }, + "devDependencies": { + "docusaurus": "^1.5.1" + } +} diff --git a/website/pages/en/index.js b/website/pages/en/index.js index f9d85fb..9f0dd0d 100755 --- a/website/pages/en/index.js +++ b/website/pages/en/index.js @@ -1,136 +1,136 @@ -/** - * Copyright (c) 2017-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -const React = require("react"); - -const CompLibrary = require("../../core/CompLibrary.js"); - -const MarkdownBlock = CompLibrary.MarkdownBlock; /* Used to read markdown */ - -const siteConfig = require(`${process.cwd()}/siteConfig.js`); - -function docUrl(doc, language) { - return `${siteConfig.baseUrl}docs/${language ? `${language}/` : ""}${doc}`; -} - -class Button extends React.Component { - render() { - return ( - - ); - } -} - -Button.defaultProps = { - target: "_self" -}; - -const SplashContainer = props => ( -
-
-
{props.children}
-
-
-); - -const ProjectTitle = () => ( -

- {siteConfig.title} - - - {siteConfig.tagline} - - -

-); - -const PromoSection = props => ( -
-
-
{props.children}
-
-
-); - -class HomeSplash extends React.Component { - render() { - const language = this.props.language || ""; - return ( - -
- - - - - - -
-
- ); - } -} - -const Features = () => ( -
-

Features

-
    -
  • Detailed file metadata and previews, especially for image, video and audio files
  • -
  • Upload status and progress, upload cancellation and restart
  • -
  • Easily set auth headers and additional upload fields
  • -
  • Customize styles using CSS or JS
  • -
  • Take full control of rendering with component injection props
  • -
  • Take control of upload lifecycle
  • -
  • Modular design; use as standalone dropzone, file input, or file uploader
  • -
  • Cross-browser support, mobile friendly, including direct uploads from camera
  • -
  • Lightweight and fast
  • -
  • Excellent TypeScript definitions
  • -
-
-); - -const Installation = () => ( -
-

Installation

- React Dropzone Uploader requires **React 16.2.0 or later.** - ```npm install --save react-dropzone-uploader``` -
-); - -class Index extends React.Component { - render() { - const language = this.props.language || ""; - - return ( -
- - - - - rdu.gif - -
- -
-
- ); - } -} - -module.exports = Index; +/** + * Copyright (c) 2017-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const React = require("react"); + +const CompLibrary = require("../../core/CompLibrary.js"); + +const MarkdownBlock = CompLibrary.MarkdownBlock; /* Used to read markdown */ + +const siteConfig = require(`${process.cwd()}/siteConfig.js`); + +function docUrl(doc, language) { + return `${siteConfig.baseUrl}docs/${language ? `${language}/` : ""}${doc}`; +} + +class Button extends React.Component { + render() { + return ( + + ); + } +} + +Button.defaultProps = { + target: "_self" +}; + +const SplashContainer = props => ( +
+
+
{props.children}
+
+
+); + +const ProjectTitle = () => ( +

+ {siteConfig.title} + + + {siteConfig.tagline} + + +

+); + +const PromoSection = props => ( +
+
+
{props.children}
+
+
+); + +class HomeSplash extends React.Component { + render() { + const language = this.props.language || ""; + return ( + +
+ + + + + + +
+
+ ); + } +} + +const Features = () => ( +
+

Features

+
    +
  • Detailed file metadata and previews, especially for image, video and audio files
  • +
  • Upload status and progress, upload cancellation and restart
  • +
  • Easily set auth headers and additional upload fields
  • +
  • Customize styles using CSS or JS
  • +
  • Take full control of rendering with component injection props
  • +
  • Take control of upload lifecycle
  • +
  • Modular design; use as standalone dropzone, file input, or file uploader
  • +
  • Cross-browser support, mobile friendly, including direct uploads from camera
  • +
  • Lightweight and fast
  • +
  • Excellent TypeScript definitions
  • +
+
+); + +const Installation = () => ( +
+

Installation

+ React Dropzone Uploader requires **React 16.2.0 or later.** + ```npm install --save react-dropzone-uploader``` +
+); + +class Index extends React.Component { + render() { + const language = this.props.language || ""; + + return ( +
+ + + + + rdu.gif + +
+ +
+
+ ); + } +} + +module.exports = Index; diff --git a/website/sidebars.json b/website/sidebars.json index 95f1b56..a08f84e 100644 --- a/website/sidebars.json +++ b/website/sidebars.json @@ -1,6 +1,6 @@ -{ - "docs": { - "Usage": ["quick-start", "api", "customization", "props", "typescript", "why-rdu"], - "Examples": ["examples", "s3"] - } -} +{ + "docs": { + "Usage": ["quick-start", "api", "customization", "props", "typescript", "why-rdu"], + "Examples": ["examples", "s3"] + } +} diff --git a/website/siteConfig.js b/website/siteConfig.js index abec662..11af157 100644 --- a/website/siteConfig.js +++ b/website/siteConfig.js @@ -1,75 +1,75 @@ -/** - * Copyright (c) 2017-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -const siteConfig = { - title: 'React Dropzone Uploader', // Title for your website. - tagline: 'The file dropzone and uploader for React', - url: 'https://react-dropzone-uploader.js.org', // Your website URL - baseUrl: '/', // Base URL for your project */ - - // Used for publishing and more - projectName: 'react-dropzone-uploader', - organizationName: 'fortana-co', - - // For no header links in the top nav bar -> headerLinks: [], - headerLinks: [ - { doc: 'quick-start', label: 'Quick Start' }, - { doc: 'api', label: 'API' }, - { doc: 'why-rdu', label: 'Why RDU?' }, - { doc: 'examples', label: 'Live Examples' }, - { search: true }, - ], - - users: [], - - /* path to images for header/footer */ - favicon: 'img/favicon.png', - - /* Colors for website */ - colors: { - primaryColor: '#2484FF', - secondaryColor: '#333333', - }, - - // This copyright info is used in /core/Footer.js and blog RSS/Atom feeds. - copyright: `Copyright © ${new Date().getFullYear()} Fortana`, - - highlight: { - // Highlight.js theme to use for syntax highlighting in code blocks. - theme: 'default', - }, - - // Add custom scripts here that would be placed in